0x10基本數據結構

0x11 棧

棧是一種後進先出的線性數據結構node

AcWing 41.包含min函數的棧

維護兩個棧,一個記錄棧的值,另外一個單調棧,記錄下當前的最小值便可
codingc++

AcWing 128. 編輯器

開兩個棧維護,相似對頂堆的操做,咱們把他叫作對頂棧好了算法

\(P\)爲光標位置,分別開兩個棧\(a,b\)數組

\(a\)\(P\)以前的數,棧\(b存\)P$以後的數數據結構

\(sum\)是前綴和,\(f\)是前綴和的最大值編輯器

對於操做\(L\),把\(x\)壓入棧\(a\)並更新\(sum\)\(f\)函數

對於操做\(D\) ,棧\(a\)棧頂彈出優化

對於操做\(L\),把棧頂\(a\)彈出並壓入棧\(b\)spa

對於操做\(R\),把棧頂\(b\)彈出並壓入棧\(a\)同時更新\(sum\)\(f\)設計

對於操做\(Q\),返回\(f[x]\)

#include <bits/stdc++.h>
using namespace std;


const int N = 1e6 + 5 , INF = 0x7ffffff;
int T , opt , a[N] , b[N] , sum[N] , f[N] , ta = 0 , tb = 0;

inline int read( bool _ )
{
    register int x = 0 , f_ = 1;
    register char ch = getchar();

    if( _ )
    {
        while( ch < '0' || ch > '9' )
        {
            if( ch == '-' ) f_ = -1;
            ch = getchar();
        }
        while( ch >= '0' && ch <= '9')
        {
            x = ( x << 3 ) + ( x << 1 ) + ch - '0';
            ch = getchar();
        }
        return x * f_;
    }
    else
    {
        while( ch != 'L' && ch != 'R' && ch != 'I' && ch != 'D' && ch != 'Q' ) ch = getchar();
        return int(ch);
    }
}

inline void work_1()
{
    a[ ++ ta ] = read(1);
    sum[ta] = sum[ ta - 1 ] + a[ta];
    f[ta] = max( sum[ta] , f[ ta - 1] );
    return ;
}

inline void work_2()
{
    if( ta > 0 ) ta --;
    return ;
}

inline void work_3()
{
    if( ta > 0 )b[ ++ tb] = a[ ta ] , ta --;
    return ;
}

inline void work_4()
{
    if( !tb ) return ;
    a[ ++ ta ] = b[tb];
    tb --;
    sum[ta] = sum[ta - 1] + a[ta];
    f[ta] = max( sum[ta] , f[ ta - 1] );
    return ;
}

inline void work_5()
{
    printf("%d\n",f[ read(1) ] );
    return ;
}


int main()
{
    f[0] = -INF;
    T = read(1);
    while( T -- )
    {
        opt = read(0);
        if(opt == 'I' ) work_1();
        else if(opt == 'D' ) work_2();
        else if(opt == 'L' ) work_3();
        else if(opt == 'R' ) work_4();
        else work_5();
    }
    return 0;
}

AcWing 131. 直方圖中最大的矩形

畫圖手玩樣例就能發現規律

單調棧的經典應用,不過我比較懶,STL+O2直接水過去

#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define LL long long
using namespace std;


const int N = 100005;
int n , now , width ;
LL res;
struct node
{
    int w , h;
}_;
stack< node > s;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline node make( int x , int y )
{
    _.h = x , _.w = y;
    return _;
}


int main()
{
    while( 1 )
    {
        n = read();
        if( !n ) return 0;
        res = 0;
        for( register int i = 1; i <= n ; i ++ )
        {
            now = read();
            if( s.empty() || now > s.top().h ) s.push( make( now , 1 ) );
            else
            {
                width = 0;
                while( !s.empty() && s.top().h > now )
                {
                    width += s.top().w;
                    res = max( res , (LL)width * s.top().h );
                    s.pop();
                }
                s.push( make( now , width + 1 ) );
            }
        }
        width = 0;
        while( !s.empty() )
        {
            width += s.top().w;
            res = max( res , (LL)width * s.top().h );
            s.pop();
        }
        printf( "%lld\n" , res );
    }
    return 0;
}

0x12 隊列

隊列是一種「先進先出」的線性數據結構,手寫隊列時能夠用循環隊列來優化空間

隊列還有一些變形體,優先隊列,單調隊列,雙端隊列,這些在\(STL\)中都是有的,不過常數比較大普通隊列手寫便可

另外優先隊列在pbds中也有

AcWing 132. 小組隊

這道題自己並不難,只是數據的處理比較噁心

首先開一個隊列爲維護小組,再開\(n\)個隊列維護每一個小組的成員

每次壓入一個元素,就把這個元素加入這個小組的隊列,若是這個小組的隊列是空的就把他加入總的隊列

每次彈出一個元素,就把總隊列隊頭的小組彈出一個,若是隊頭小組的隊列此時爲空,就把隊頭小組從總隊列總彈出

這道題並非十分的卡常數,不開\(O2\)貌似能過,

另外插隊不是好習慣,當心被打

#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;


const int N = 1e6 + 5 , M = 1005;
int n , t , m , num , cub[N];
string opt;
map< int , queue<int> > member;
queue< int > team;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void push()
{
    num = read();
    if( member[ cub[num] ].empty() ) team.push( cub[num] );
    member[ cub[num] ].push( num );
    return ;
}

inline void pop()
{
    num = team.front();
    printf( "%d\n" , member[ num ].front() );
    member[ num ].pop();
    if( member[ num ].empty() ) team.pop();
}

inline void work( int k )
{
    n = read();
    if( !n ) exit(0);
    printf( "Scenario #%d\n" , k );

    while( !team.empty() )
    {
        num = team.front();
        while( !member[ num ].empty() ) member[ num ].pop();
        team.pop();
    }
    memset( cub , 0 , sizeof(cub) );

    for( register int i = 1 ; i <= n ; i ++ )
    {
        t = read();
        while( t -- )  cub[ read() ] = i;
    }

    while( 1 )
    {
        cin >> opt;
        if( opt == "ENQUEUE" ) push();
        else if( opt == "DEQUEUE" ) pop();
        else break;
    }
    puts("");
    return ;
}


int main()
{
    for( register int k = 1 ; 1 ; k ++ ) work(k);
    return 0;
}

AcWing 135. 最大子序和

單調隊列的基操

首先對於區間和的問題通常狀況下都是轉發乘前綴和數組,作差便可

而後就是找左右端點的問題

令前綴和數組爲\(s\)

已經枚舉的右端點\(i\)和當前的左端點\(j\)

此時再任意一個\(k\)若是知足\(k<j<i\)\(s[k]>s[j]\),着\(k\)不管如何也不可能成爲最有解,由於對於任意的\(i\)若是能夠選\(j\)\(j\)必定\(k\)更優

因此咱們發現須要維護一個單調遞增的序列,而且隨着\(i\)的有移,將會有部分的\(j\)不能使用

符合單調隊列的性質因此用單調隊列來維護,隊列儲存的元素是前綴和數組的下標,隊頭爲\(l\),隊尾爲\(r\)

對於每次枚舉的\(i\)有如下幾個操做

  1. 若是\(q[l] < i - m\)將隊頭出對
  2. 此時的\(l\)就是最有的\(j\)更新答案
  3. 維護單調隊列性質並把\(i\)放入隊列
#include <bits/stdc++.h>
using namespace std;


const int N = 300000;
int n , m , s[N] , q[N] , l  = 1 , r = 1 , res ;


inline int read()
{
    register int x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 ; i <= n ; i ++ ) s[i] = s[i-1] + read();

    for( register int i = 1 ; i <= n ; i ++ )
    {
        while( l <= r && q[l] < i - m ) l ++;
        res = max( res , s[i] - s[ q[l] ] );
        while( l <= r && s[ q[r] ] >= s[i] ) r --;
        q[ ++ r ] = i;
    }
    cout << res << endl;
    return 0;
}

0x13鏈表與鄰接表

數組是一種支持隨機訪問,但不支持在任意位置插入或刪除元素的數據結構

鏈表支持在任意位置插入或刪除,但只能按順序訪問其中的元素

鏈表的正規形式通常是經過動態分配內存、指針實現,爲了不內存泄漏、方便調試使用數組模擬鏈表、下標模擬指針也是常見的作法

指針版

struct Node {
    int value; // data
    Node *prev, *next; // pointers
};
Node *head, *tail;

void initialize() { // create an empty list
    head = new Node();
    tail = new Node();
    head->next = tail;
    tail->prev = head;
}

void insert(Node *p, int value) { // insert data after p
    q = new Node();
    q->value = value;
    p->next->prev = q; q->next = p->next;
    p->next = q; q->prev = p;
}

void remove(Node *p) { // remove p
    p->prev->next = p->next;
    p->next->prev = p->prev;
    delete p;
}

void recycle() { // release memory
    while (head != tail) {
        head = head->next;
        delete head->prev;
    }
    delete tail;
}

數組模擬

struct Node {
    int value;
    int prev, next;
} node[SIZE];
int head, tail, tot;

int initialize() {
    tot = 2;
    head = 1, tail = 2;
    node[head].next = tail;
    node[tail].prev = head;
}

int insert(int p, int value) {
    q = ++tot;
    node[q].value = value;
    node[node[p].next].prev = q;
    node[q].next = node[p].next;
    node[p].next = q; node[q].prev = p;
}

void remove(int p) {
    node[node[p].prev].next = node[p].next;
    node[node[p].next].prev = node[p].prev;
}


// 鄰接表:加入有向邊(x, y),權值爲z
void add(int x, int y, int z) {
    ver[++tot] = y, edge[tot] = z; // 真實數據
    next[tot] = head[x], head[x] = tot; // 在表頭x處插入
}

// 鄰接表:訪問從x出發的全部邊
for (int i = head[x]; i; i = next[i]) {
    int y = ver[i], z = edge[i];
    // 一條有向邊(x, y),權值爲z
}

AcWing 136. 鄰值查找

首先咱們開一個pair記錄\(A_i\)和對應的\(i\)

而後排序,並用一個鏈表維護這個序列,鏈表的值是每一個數字排序後的位置

因此每一個鏈表的前驅就是小於等於這個數中最大的,後繼就是大於等於這個數中最小的

而後咱們倒着訪問從\(n\)開始,由於這樣無論是前驅仍是後繼在原序列中的位置必定比當前數在原序列中的位置跟靠前

作差比較、記錄結果

而後刪掉當前這個數字,由於剩下的數字在原序列中都比他靠前,因此這個數字必定不會是其餘數字的結果

#include <bits/stdc++.h>
#define LL long long
using namespace std;


const int N = 1e5 + 5 , INF = 0x7f7f7f7f;
int n , l[N] , r[N] , p[N];
pair< int ,int > a[N] , res[N];


inline int read()
{
    register int x = 0,f = 1;
    register char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}

int main()
{
    n = read();
    for( register int i = 1 ; i <= n ; i ++ )
    {
        a[i].first = read();
        a[i].second = i;
    }
    
    sort( a + 1 , a + 1 + n );
    
    a[0].first = -INF , a[ n + 1 ].first = INF;
    
    for( register int i = 1 ; i <= n ; i ++ ) l[i] = i - 1 ,  r[i] = i + 1 , p[ a[i].second ] = i;
    
    for( register int i = n ; i > 1 ; i -- )
    {
        register int j = p[i] , L = l[j] , R = r[j] ;
        register LL l_val = abs( a[L].first - a[j].first ) , r_val = abs( a[R].first - a[j].first );
        if( l_val <= r_val ) res[i].first = l_val , res[i].second = a[L].second;
        else res[i].first = r_val , res[i].second = a[R].second;
        l[R] = L , r[L] = R;
    }
    
    for( register int i = 2 ; i <= n ; i ++ ) printf( "%d %d\n" , res[i].first , res[i].second );
    
    return 0;
}

0x14 Hash

Hash 表

Hash表 又稱散列表,通常有Hash函數與鏈表結構共同構成

Hash表主要包括兩個基本操做

  1. 計算Hash函數的值
  2. 定位到對應的鏈表中依次遍歷、比較

經常使用的的Hash函數是\(H(x) = (x\mod \ p)+ 1\)

這樣顯然能夠把全部的數分紅\(p\)個,若是遇到衝突狀況,用鏈表維護便可

AcWing 137. 雪花雪花雪花

設計Hash函數爲\(H(a_1,a_2,\cdots,a_6) = (\sum^{6}_{i=1}a_i + \Pi^{6}_{i=1}a_i)\ mod\ p\),其中\(p\)是一個咱們本身選擇的一個大質數

而後咱們依次把每一個雪花插入Hash表中,在對應的鏈表中查找是否已經有相同的雪花

判斷是否有相同雪花的方式就是直接暴力枚舉就好

#include <bits/stdc++.h>
using namespace std;


const int N = 100010,p = 9991;
int n ,head[N] , nxt[N] ,snow[N][6], tot;


inline int  H( int *a )
{
    int sum = 0 , mul = 1 ;
    for( register int i = 0 ; i < 6 ; i ++ ) sum = ( sum + a[i] ) % p , mul = ( ( long long )mul * a[i] ) % p;
    return ( sum + mul ) % p;  
}

inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline bool equal( int *a, int *b)
{
    for( register int i = 0 ; i < 6 ; i ++ )
    {
        for( register int j = 0 ; j < 6 ; j ++ )
        {
            bool eq = 1;
            for( register int k = 0 ; k < 6 && eq; k ++ )
            {
                if( a[ ( i + k ) % 6 ] != b[ ( j + k ) % 6 ] ) eq = 0; 
            }
            if( eq ) return 1;
            eq = 1;
            for( register int k = 0 ; k < 6  && eq; k ++ )
            {
                if( a[ ( i + k ) % 6 ] != b[ ( j - k + 6 ) % 6 ] ) eq = 0;
            }
            if ( eq ) return 1;
        }
    }
    return 0;
} 

inline bool insert( int *a )
{
    register int val = H( a );
    for( register int i = head[val] ; i ; i = nxt[i] )
    {
        if(equal(snow[i] , a ) ) return 1;
    }
    ++ tot;
    memcpy( snow[tot] , a , 6 * sizeof( int ) );
    nxt[ tot ] = head[val];
    head[val] = tot;
    return 0;
}


int main()
{
    n = read();
    int a[10];
    
    for( register int j = 1 ; j <= n ; j ++ )
    {
        for( register int i = 0 ; i < 6 ; i ++ ) a[i] = read();
        if( !insert( a ) ) continue;
        puts( "Twin snowflakes found." );
        exit(0); 
    }
    
    puts( "No two snowflakes are alike." );
    return 0;
}

字符串Hash

下面介紹的字符串\(Hash\)函數把任意一個長度的支付串映射成一個非負整數,而且衝突的機率近乎爲\(0\)

取一固定值\(P\),把字符串當作是\(P\)進制數而且分配一個大於\(0\)的數值,表明每種字符。通常說,咱們分配的數值都遠小於\(P\)。例如,對於小寫字母構成的字符串,能夠令\(a=1,b=2,\dots ,z = 26\)。取一固定值M,求出該P進制數對M取的餘數,做爲該字符的\(Hash\)值。

通常來講,咱們取\(P=131\)\(P=13331\),此時\(Hash\)值產生的衝突機率極低,一般咱們取\(M=2^{26}\),即直接使用\(unsigned\ long\ long\)的天然溢出來代替低效率的取模運算。

可是在極端構造的數據中取模會致使\(Hash\)衝突,因此能夠採用鏈表來存下每一個字符串,也能夠經過屢次\(Hash\)來解決

AcWing 140. 後綴數組

這道題是字符串Hash,首先把原字符串的前綴進行Hash

而後用一個數組來表明後綴,經過\(O(1)\)計算獲得後綴的Hash

而後在比較時,咱們經過二分,二分出兩個後綴的最大公共前綴,咱們只需比較公共前綴的下一位就能夠比較兩個後綴的字典序

#include <bits/stdc++.h>
#define ULL unsigned long long
#define H( l , r ) ( h[r] - h[ l - 1 ] * p[ r - l + 1 ] ) 
using namespace std;


const int N = 300010 , base = 131;
int n ,sa[N];
ULL h[N] , p[N];
char str[N];


inline ULL get_max_common_prefix( int a , int b )
{
    int l = 0 , r = min( n - a + 1 , n - b + 1 );
    while( l < r )
    {
        int mid = l + r + 1 >> 1;
        if( H( a , a + mid - 1 ) != H( b , b + mid - 1 ) ) r = mid - 1;
        else l = mid;
    }
    return l;
}

inline bool cmp( int a , int b)
{
    register int l = get_max_common_prefix( a , b );
    register int av = a + l > n ? INT_MIN : str[ a + l ];
    register int bv = b + l > n ? INT_MIN : str[ b + l ]; 
    return av < bv;
}


int main()
{
    scanf( "%s" , str + 1 );
    n = strlen( str + 1 );
    
    p[0] = 1 ;
    for( register int i = 1 ; i <= n ; i ++ )
    {
        p[i] = p[ i - 1 ] * base;
        h[i] = h[ i - 1 ] * base + str[i] - 'a'  + 1 ;
        sa[i] = i;
    }
    
    sort( sa + 1 , sa + 1 + n , cmp );
    
    for( register int i = 1 ;i <= n ; i ++ ) printf("%d " , sa[i] - 1 );
    puts("");
    
    for( register int i = 1; i <= n ;i ++ )
    {
        if( i == 1 ) printf( "0 " );
        else printf( "%d " , get_max_common_prefix( sa[ i - 1 ] , sa[i] ) );    
    }
    puts("");
    
    return 0;
}

0x15 字符串

KMP模式匹配

\(KMP\)算法,又稱模式匹配算法,可以在線性時間內斷定字符串\(A[1\dots N]\)是不是字符串\(B[1\dots M]\)的子串,並求出字符串\(A\)在字符串\(B\)中出現的位置

KMP算法分爲兩步

  1. 對字符串A進行自我匹配,求出一個數組\(next\),其中\(next[i]\)表示「\(A\)中以\(i\)結尾的非前綴子串」與「\(A\)的前綴」可以匹配的最長長度,即:

    \(next[i] = max\{ j \}\),其中\(j<i\)\(A[i-j+1\dots i] = A[1\dots j]\)

    特別地,當不存在這樣的\(j\)\(next[i] = 0\)

  2. 對於字符串\(A\)\(B\)進行匹配,求出一個數組\(f\),其中\(f[i]\)表示「\(B\)中以\(i\)結尾的子串」與「\(A\)的前綴」可以匹配的最長長度,即:

    \(f[i] = max\{ j \}\),其中\(j\le i\)\(B[i-j+1\dots i] = A[1\dots j]\)

\(KMP\)算法\(next\)數組的求法

next[1] = 0;
for( register int i = 2 , j = 0 ; j <= n ; i ++ )
{
        while( j && a[i] != a[ j + 1 ] ) j = next[j];
        if( a[i] == a[ j + 1 ] ) j ++ ;
        next[i] = j; 
}

\(KMP\)算法\(f\)數組的求法

for( register int i = 1 , j = 0 ; i <= m ; i ++ )
{
        while( j && ( j == n || b[i] != a[ j + 1 ] ) ) j = next[j];
        if( b[i] == a[ j + 1 ] ) j ++;
        f[i] = j;
        if( f[i] == n ) //此時就是A在B中間出現一次
}

CF1029A

這道題實際上就是一道啊很簡單的\(KMP\)模板題,理解下\(KMP\)\(next\)數組的做用就明白了

先輸出原序列,在把\(t[next[n]\cdots n]\)輸出\(k-1\)次就好

#include <bits/stdc++.h>
using namespace std;


const int N = 60;
int n , k , nxt[N];
char t[N];

int main()
{
    cin >> n >> k;
    scanf( "%s" , t + 1 );
    
    nxt[1] = 0;
    for( register int i = 2 , j = 0 ;i <= n ; i ++ )
    {
        while( j && t[i] != t[ j + 1 ] ) j = nxt[j];
        if( t[i] == t[j + 1] ) j ++ ;
        nxt[i] = j ;
    }   

    printf( "%s" , t + 1 );
    
    for( ; k > 1 ; k -- ) 
    {
        for( register int i = nxt[n] + 1 ; i <= n ; i ++ ) printf( "%c" , t[i] );
    }
    puts("");
    return 0;
}

最小表示法

給定一個字符串\(S[1\dots n]\),若是咱們不斷的把它的最後一個字符放到開頭,最終會獲得\(n\)個字符串,稱這\(n\)個字符串是循環同構的。這些字符串中字典序最小的一個稱爲字符串\(S\)的最小表示法

算法流程

  1. 初始化i=1,j=2
  2. 經過直接前後掃描的方法比較 b[i]與b[j]兩個循環同構串。
    • 若是掃描了n個字符後仍然相等,說明s有更小的循環元(例如catcat有循環元cat),而且該循環元以掃描完成,B[min(i,j)]即爲最小表示,算法結束
    • 若是在i+k與j+k處發現不想等:
      • 若ss[i+k]>ss[j+k],令i=i+k+1。若此時i=j,再令i=i+1
      • 若ss[i+k]<ss[j+k],令j=j+k+1。若此時i=j,再令j=j+1
  3. 若i>n或j>n,則B[min(i,j)]爲最小表示;不然重複第二步
int  n = strlen( s + 1 ); 
for( register int i = 1 ; i <= n ; i ++ ) s[ n + i ] = s[i];
int i = 1 , j = 2 , k;
while( i <= n && j <= n )
{
    for( k = 0 ; k < n && s[ i + k ] == s[ j + k ] ; k ++ );
    if( k == n ) break;//s形如 catcat ,它的循環元以掃描完成
    if( s[ i + k ] > s[ j + k ] )
    {
        i += k + 1;
        if( i == j ) i ++;
    }
    else 
    {
        j += k + 1;
        if( i == j ) j ++; 
    }
}
ans = min( i , j ); //B[ans]是s的最小表示

Luogu P1368

看題目,簡單分析就知道是落得最小表示法

#include <bits/stdc++.h>
#define LL long long
using namespace std;


const int N = 300005 * 2 ;
int n , ans;
LL a[N];


inline LL read()
{
    register LL x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void mini_notation()
{
    register int i = 1 , j = 2 , k;
    while( i <= n && j <= n )
    {
        for( k = 0 ; k < n && a[ i + k ] == a[ j + k ] ; k ++ );
        if( k == n ) break;
        if( a[ i + k ] <= a[ j + k ] )
        {
            j += k + 1;
            if( i == j ) j ++;
        }
        else
        {
            i += k + 1;
            if( i == j ) i ++;
        }
    }
    ans = min( i , j );
}


int main()
{
    n = read();
    for( register int i = 1 ; i <= n ; i ++ ) a[ i + n ] = a[i] = read();
    
    mini_notation();
    
    for( register int i = ans , j = 1 ; j <= n ; i ++ , j ++ ) printf( "%lld " , a[i] );
    puts("");
    return 0;
    
}

0x16 Trie

Trie,又稱字典樹,是一種用於實現字符串快速檢索的多叉樹結構。Trie的每一個節點都擁有若干個字符指針,若在插入或檢索字符串時掃描到一個字符c,就沿着當前節點的這個字符指針,走向該指針指向的節點。

Trie的節點可使用一個結構體進行儲存,以下代碼中,trans[i]表示這個節點邊上的之父爲i的邊到達兒子節點的編號,若爲0則表示沒有這個兒子節點

struct node
{
    int trans[z];// z爲字符集的大小
    bool bo;// 若bo = true 則表示這個頂點表明的字符串是集合中的元素 
}tr[N];

如今要對一個字符集的Trie插入一個字符串s

inline void insert(string s)
{
    register int len = s.size(),u = 1;
    for(register int i = 0;i < len;i ++)
    {
        if(!tr[u].trans[s[i] - 'a']) tr[u].trans[s[i] - 'a'] = ++ tot;
        //若不存在這條邊則要創建一個新的節點 tot爲總的點數 
        u = tr[u].trans[s[i] - 'a']; 
    }
    tr[u].bo = 1; //在結尾表示它表明的字符串是集合中的一個元素 
    return ;
}

查詢一個字符串s是否在集合中某個串的前綴

inline bool search(string s)
{
    register int len = s.size(),u = 1;
    for(register int i = 0;i < len; i ++)
    {
        if(!tr[u].trans[s[i] - 'a']) return 0;
        u = tr[u].trans[s[i] - 'a'];
    }
    return 1;
}

查詢一個字符串s是不是集合中的一個元素

inline bool query(string s)
{
    register int len = s.size(),u = 1;
    for(register int i = 0;i < len; i ++)
    {
        if(!tr[u].trans[s[i] - 'a']) return 0;
        u = tr[u].trans[s[i] - 'a'];
    }
    return tr[u].bo;
}

AcWing 142. 前綴統計

構建一顆\(tire\)樹在每一個結點存一個\(cn\)t記錄以當前節點爲結尾的字符串有多少個

而後在遍歷\(tire\)樹將\(cnt\)求和便可

#include <bits/stdc++.h>
#define I( x ) ( x - 'a' )
using namespace std;


const int N = 1e6 + 5 , Z = 30;
int n , m , tot = 1 , len , u , ans ;
string s;

struct node
{
    int cnt , trans[Z];
}tr[N];


inline void insert()
{
    len = s.size() , u = 1;
    for( register int i = 0 ; i < len ; i ++ )
    {
        if( !tr[u].trans[ I( s[i] ) ] ) tr[u].trans[ I( s[i] ) ] = ++ tot;
        u = tr[u].trans[ I( s[i] ) ];
    }
    tr[u].cnt ++;
    return ;
}

inline int search()
{
    len = s.size() , u = 1 ,ans = 0;
    for( register int i =  0 ; i < len ; i ++ )
    {
        if(!tr[u].trans[ I( s[i] ) ] ) return ans;
        u = tr[u].trans[ I( s[i] ) ];
        ans += tr[u].cnt;
    }
    return ans;
}


int main()
{
    cin >> n >> m;
    for( register int i = 1 ; i <= n ; i ++ ) 
    {
        cin >> s;
        insert();
    }
    
    for( register int i = 1 ; i <= m ; i ++ )
    {
        cin >> s;
        cout << search() << endl;   
    }
    
    return 0;
}

AcWing 143.最大異或對

要寫這道題首先要了解一些位運算的相關知識

首先咱們能夠構建一個\(01tire\),把全部的數字轉化成二進制插入

而後咱們枚舉一下每個數字,而後去\(01tire\)中查找,查找每一位時,首先查找是否有和當前位相反的,若是有就選擇

這樣查找完後,獲得二進制數就是全部數字中和當前數異或值最大的,對全部的最大值取\(max\)便可

觀察發現,咱們能夠一遍建樹,一邊查找,效果是同樣的

#include <bits/stdc++.h>
using namespace std;


const int N = 1e5 + 5;
int n , a[N] , tot = 1 , res = -1;

struct Trie
{
    int to[2];
}t[ N * 32 ];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void insert( int x )
{
    register int u = 1 , s;
    for( register int i = 30 ; i >= 0 ; i -- )
    {
        s = x >> i & 1 ;
        if( !t[u].to[s] ) t[u].to[s] = ++ tot;
        u = t[u].to[s];
    }
}
 
inline int search( int x )
{
    register int u = 1 , ans = 0 , s;
    for( register int i = 30 ; i >= 0 ; i -- )
    {
        s =  x >> i & 1;
        if( t[u].to[ s ^ 1 ] ) u = t[u].to[ s ^ 1 ] , ans |= 1 << i;
        else u = t[u].to[s];
    }
    return ans;
}


int main()
{
    n = read();
    
    for( register int i = 1 ; i <= n ; i ++ ) a[i] = read() , insert( a[i] ) , res = max( res , search( a[i] ) );
    
    cout << res << endl;

    return 0;
}

0x17 二叉堆

二叉堆是一種支持插入、刪除、查詢最值的數據結構。它實際上是一顆知足「堆性質」的徹底二叉樹

二叉樹的實現能夠手寫,固然我本身跟推薦使用STL,固然pbds也能夠

priority_queue

構造

priority_queue< int > q;//大根堆
priority_queue< int , vector< int > , greater< int > > q;//小根堆

注意priority_queue中儲存的元素類型必須定義「小於號」,較大的元素會被放在堆頂。內置的int、string等類型已經定義過「小於號」,若使用結構體則必須重載運算符

因爲priority_queue是按照從大到小排序因此重載運算符時也要反過來

struct node
{
    int value ;
    friend bool operator < (node a , node b)
    {
        return a.value > b.value;
    }
};

成員函數

q.top();\\訪問堆頂元素
q.empty();\\檢查是否爲空
q.size();\\返回容納的元素數
q.push();\\插入元素,並排序
q.pop();\\刪除棧頂元素

懶惰刪除法

若是是手寫的堆是支持刪除任意一個元素,而\(STL\)卻不支持這種操做因此咱們能夠用懶惰刪除法

懶惰刪除法又稱延遲刪除法,是一種應對策略。當遇到刪除操做時,僅在優先隊列以外作一些特殊的記錄,用於辨別是否堆中的元素被刪除。當從堆頂取出元素時判斷是否已經被刪除,如果,咱們從新取一個最值。換言之,元素的「刪除」推遲到堆頂執行

好比「堆優化的\(Dijkstra\)算法」中當某個元素首次被取出時就達到了最短路,當咱們再次取出這個元素時咱們不會從新進行擴展,而是使用一個\(bool\)數組判斷「是否進行過擴展」,其本質仍是懶惰刪除法的應用

AcWing 146. 序列

首先這道題目,咱們能夠先考慮\(m=2\)的這種特殊狀況

咱們發現,當\(A\)序列和\(B\)序列從小到大排序後,最小和確定是\(A[1]+B[1]\),而次小和必然是\(min(A[2]+B[1],A[1]+B[2])\),也就是說當咱們肯定好\(A[i][j]\)\(K\)小和的話,那麼第\(k+1\)小的和,必然是\(min(A[k+1]+B[k],A[k]+B[k+1])\),既然如此的話,咱們還要注意一點,\(A[1]+B[2]\)\(A[2]+B[1]\)均可以推導出\(A[2]+B[2]\),因此說咱們要記得,若是說\(j+1\)了,那麼i就不要\(+1\)了,避免出現重疊,致使答案錯誤.至於\(min\)函數,可使用小根堆來維護當前最小值.

數學的概括法,咱們就能夠從\(2\),推到\(N\)的狀況,也就是先求出前兩個序列的值,而後推出前\(N\)小的和的序列,而後用這個退出來的序列,再和第三個序列求值,而後同理,再得出來的值與第四個序列進行一樣的操做

#include <bits/stdc++.h>
using namespace std;


const int N = 2010;
int t , n , m , a[N] , b[N] , c[N] , tot;

struct node
{
    int i , j;
    bool f;
    friend bool operator < ( node x , node y )
    {
        return a[ x.i ] + b[ x.j ] > a[ y.i ] + b[ y.j ];
    }

}cur , temp ;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline node make_node( int i , int j , bool f )
{
    cur.i = i , cur.j = j , cur.f = f;
    return cur;
}

inline void work()
{
    sort( b + 1 , b + 1 + m );
    priority_queue< node > q;
    tot = 0;
    
    q.push( make_node( 1 , 1 , 0 ) );
    for( register int i = 1 ; i <= m ; i ++)
    {
        temp = q.top() , q.pop();
        c[i]  = a[ temp.i ] + b[ temp.j ];
        q.push( make_node( temp.i , temp.j + 1 , 1 ) );
        if( !temp.f ) q.push( make_node( temp.i + 1 , temp.j , 0 ) );
    }
    memcpy( a , c , sizeof( a ) );
    return ;
}


int main()
{
    
    t = read();
    while(t--)
    {
        n = read() , m = read();
        for( register int i = 1 ; i <= m ; i ++ ) a[i] = read();
        sort( a + 1 , a + 1 + m );  
        
        for( register int i = 2 ; i <= n ; i ++ )
        {
            for( register int j = 1 ; j <= m ; j ++ ) b[j] = read();
            work();
        }
    
        for( register int i = 1 ; i <=  m  ; i ++ ) printf( "%d " , a[i] );
        puts("");   
    }
    return 0;
}

AcWing 147. 數據備份

Luogo P3620 數據備份

這是一道貪心+鏈表+堆的題

對於題面其實很好理解,就是有\(n\)個點,\(n-1\)條邊,從中選\(k\)個可是每一個節點只能選一次,求邊權最小和

首先咱們求\(k = 1\)時的狀況,即全部邊中最小的一個

再看\(k=2\)的狀況,首先咱們選擇的全部中最小的一個即爲\(i\)

呢麼第二條選的不是\(i-1\),或\(i+1\)則無影響

若第二條邊選的時\(i-1\)\(i+1\)必選,也就是放棄\(i\)

由於若是選\(i-1\),不選\(i+1\)\(j\)的狀況下,此時對\(i\)時沒有限制的則必有\(v[i]+v[k]\le v[i-1]+v[k]\)

若是\(k=3\),舉下面這個例子

假設已經選擇的\(2\)\(4\)

此時咱們要選擇\(1\)則必選\(3\)\(5\)

若是不選\(3,5\),選\(3,6\)的話

則必有\(1,4,6\)\(1,3,6\)更優

根據數學概括法咱們能夠推出,若是咱們已經選擇一串連續的點構成的邊,假如咱們由於要選擇某一條邊來破壞某一條邊已經被選擇的邊,呢麼這些連續的點構成的邊必定要所有都破壞否則不可能更優

知道這個結論後在結合貪心的策略就能夠解決這個問題

首先咱們用一個堆來維護因此的邊首先取出一個邊\(i\),把\(v[i]\)累加的答案中,而且在堆中加入條權值爲\(v[i-1]+v[i+1]-v[i]\),左端點爲\(i-1\)的左端點,右端點爲\(i+1\)的右端點的邊,而且刪除\(i-1\)\(i+1\)這兩條邊

這樣當咱們選擇的到\(i-1\)\(i+1\)時都會選擇到這條新加入邊,對位邊的信息咱們用雙向鏈表來維護便可

對於堆的刪除操做可使用懶惰標記法,這裏給出一個\(set\)解決的方法,並會在下一小節給出set的基本用法

#include <bits/stdc++.h>
#define LL long long
#define PLI pair< LL , int >
using namespace std;


const int N = 100010;
int n , k , l[N] , r[N];
LL d[N] , res;
set< PLI > s;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}


inline void delete_node( int p )
{
    r[ l[p] ] = r[p] , l[ r[p] ] = l[p];
}



int main()
{
    n = read() , k = read();
    for( register int i = 1 ; i <= n ; i ++ ) d[i] = read();
    for( register int i = n ; i > 0 ; i -- ) d[i] -= d[ i - 1 ];
    d[1] = d[ n + 1 ] = 1e15;
    
    for( register int i = 1 ; i <= n ; i ++ )
    {
        l[i] = i - 1;
        r[i] = i + 1;
        s.insert( { d[i] , i } );
    }
    
    while( k -- )
    {
        set< PLI >::iterator it = s.begin();
        register LL v = it -> first;
        register int p = it -> second , left = l[p] , right = r[p];
        s.erase(it) , s.erase( { d[left] , left } ) , s.erase( { d[right] , right } );
        delete_node(left) , delete_node(right);
        res += v;
        d[p] = d[left] + d[right] - d[p];
        s.insert( { d[p] , p } ) ;
    }
    
    cout << res << endl;
    return 0;
}

set

set< int > s;//構造函數,元素不可重複 
multiset<int>s;//構造函數,元素能夠重複 
s.size();//返回s中有多少元素 
s.empty();//返回s是否爲空 
s.clear();//清空s 
s.begin();//返回指向s中第一個元素的迭代器 
s.end();//返回指向s中最後一個元素下一個位置的迭代器 
s.insert(x);//向s中插入一個元素x 
s.find(x);//返回s中指向x的迭代器,若是s中沒有x則返回s.end() 
s.erase(x);//刪除x 
s.count(x)//返回s中x元素的個數(這個只適用於multiset)

Huffman 樹

考慮這樣一個問題:構造一顆包含\(n\)個節點的\(k\)叉樹,其中第\(i\)個葉子節點的權值爲\(w_i\),要求最小化\(\sum w_i \times l_i\)其中\(l_i\)表示第\(i\)個葉子節點到根節點的距離

該問題被稱爲Huffman樹(哈夫曼樹)

爲了最小化\(\sum w_i \times l_i\),應該讓權值打的葉子節點的深度儘可能的小。當\(k=2\)時,咱們很容易想到用下面這個貪心思路求\(Huffman\)

  1. 創建一個小根堆,插入這\(n\)個葉子節點的權值
  2. 從隊列中取出兩個最小的權值\(w_1\)\(w_2\),令\(ans += w_1 + w_2\)
  3. 創建一個權值爲\(w_1 + w_2\)的樹節點\(p\),並把\(p\)成爲\(w_1\)\(w_2\)的父親節點
  4. 在堆中插入\(p\)節點
  5. 重複\(2 \cdots 4\),直到堆的大小爲\(1\)


對於\(k>2\)\(Huffman\)樹,正常的想法就是在上述算法上每次取出\(k\)的節點

但加入最後一次取不出\(k\)個時,也就是第一層未滿,此時從下方任意取出一個子樹接在根節點的下面都會更優

因此咱們要進行一些操做

咱們插入一些額外的權值爲\(0\)的葉子節點,知足\((n-1)mod(k-1) = 0\)

這是在根據上述思路作便可,由於補\(0\)後只有最下面的一次是不滿的

AcWing 148. 合併果子

\(2\)\(Huffman\)樹模板題,直接作便可

#include <bits/stdc++.h>
using namespace std;

int n , ans , a , b;
priority_queue< int , vector<int> , greater<int> > q; 


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3 ) + ch - '0';
        ch = getchar();
    }
    return x;
} 


int main()
{
    n = read();
    for( register int i = 1 ; i <= n ; i ++ ) q.push( read() );
    while( q.size() > 1 )
    {
        a = q.top() , q.pop() , b = q.top() , q.pop();
        ans += a + b;
        q.push( a + b );
    }
    cout << ans << endl;
    return 0;
}

AcWing 149. 荷馬史詩

這道題目背景比多,有考閱讀的成分

簡化版的提議就是求\(Huffman\)樹,而且求出\(Huffman\)樹的深度

因此只需稍做更改便可

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair< LL, int> PLI;


int n , m ;
LL res; 
priority_queue< PLI , vector<PLI> , greater<PLI> > heap;


inline LL read()
{
    register LL x = 0 , f = 1;
    register char ch = getchar();
    while( ch < '0' || ch > '9' )
    {
        if( ch == '-' ) f = -1;
        ch = getchar();
    }
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x * f;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 ; i <= n ; i ++ ) heap.push( { read() , 0 } );
    
    while( ( n - 1 ) % ( m - 1 ) ) heap.push( { 0ll , 0 } ) , n ++;
    
    while( heap.size() > 1 )
    {
        register LL sum = 0;
        register int depth = 0;
        for( register int i = 0 ; i < m ; i ++ ) sum += heap.top().first , depth = max( depth , heap.top().second ) , heap.pop();
        res += sum;
        heap.push( { sum , depth + 1 } );
    }
    
    cout << res << '\n' << heap.top().second << '\n';
    return 0;
}
相關文章
相關標籤/搜索