栈是一种后进先出的线性数据结构node
维护两个栈,一个记录栈的值,另外一个单调栈,记录下当前的最小值便可
codingc++
开两个栈维护,相似对顶堆的操做,咱们把他叫作对顶栈好了算法
令\(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; }
画图手玩样例就能发现规律
单调栈的经典应用,不过我比较懒,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; }
队列是一种“先进先出”的线性数据结构,手写队列时能够用循环队列来优化空间
队列还有一些变形体,优先队列,单调队列,双端队列,这些在\(STL\)中都是有的,不过常数比较大普通队列手写便可
另外优先队列在pbds中也有
这道题自己并不难,只是数据的处理比较恶心
首先开一个队列为维护小组,再开\(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; }
单调队列的基操
首先对于区间和的问题通常状况下都是转发乘前缀和数组,作差便可
而后就是找左右端点的问题
令前缀和数组为\(s\)
已经枚举的右端点\(i\)和当前的左端点\(j\)
此时再任意一个\(k\)若是知足\(k<j<i\)且\(s[k]>s[j]\),着\(k\)不管如何也不可能成为最有解,由于对于任意的\(i\)若是能够选\(j\)则\(j\)必定\(k\)更优
因此咱们发现须要维护一个单调递增的序列,而且随着\(i\)的有移,将会有部分的\(j\)不能使用
符合单调队列的性质因此用单调队列来维护,队列储存的元素是前缀和数组的下标,队头为\(l\),队尾为\(r\)
对于每次枚举的\(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; }
数组是一种支持随机访问,但不支持在任意位置插入或删除元素的数据结构
链表支持在任意位置插入或删除,但只能按顺序访问其中的元素
链表的正规形式通常是经过动态分配内存、指针实现,为了不内存泄漏、方便调试使用数组模拟链表、下标模拟指针也是常见的作法
指针版
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 }
首先咱们开一个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; }
Hash表 又称散列表,通常有Hash函数与链表结构共同构成
Hash表主要包括两个基本操做
经常使用的的Hash函数是\(H(x) = (x\mod \ p)+ 1\)
这样显然能够把全部的数分红\(p\)个,若是遇到冲突状况,用链表维护便可
设计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\)函数把任意一个长度的支付串映射成一个非负整数,而且冲突的几率近乎为\(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\)来解决
这道题是字符串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; }
\(KMP\)算法,又称模式匹配算法,可以在线性时间内断定字符串\(A[1\dots N]\)是不是字符串\(B[1\dots M]\)的子串,并求出字符串\(A\)在字符串\(B\)中出现的位置
KMP算法分为两步
对字符串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\)
对于字符串\(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中间出现一次 }
这道题实际上就是一道啊很简单的\(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\)的最小表示法
算法流程
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的最小表示
看题目,简单分析就知道是落得最小表示法
#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; }
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; }
构建一颗\(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; }
要写这道题首先要了解一些位运算的相关知识
首先咱们能够构建一个\(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; }
二叉堆是一种支持插入、删除、查询最值的数据结构。它实际上是一颗知足“堆性质”的彻底二叉树
二叉树的实现能够手写,固然我本身跟推荐使用STL,固然pbds也能够
构造
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\)数组判断“是否进行过扩展”,其本质仍是懒惰删除法的应用
首先这道题目,咱们能够先考虑\(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; }
这是一道贪心+链表+堆的题
对于题面其实很好理解,就是有\(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< 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)
考虑这样一个问题:构造一颗包含\(n\)个节点的\(k\)叉树,其中第\(i\)个叶子节点的权值为\(w_i\),要求最小化\(\sum w_i \times l_i\)其中\(l_i\)表示第\(i\)个叶子节点到根节点的距离
该问题被称为Huffman树(哈夫曼树)
为了最小化\(\sum w_i \times l_i\),应该让权值打的叶子节点的深度尽可能的小。当\(k=2\)时,咱们很容易想到用下面这个贪心思路求\(Huffman\)树
对于\(k>2\)的\(Huffman\)树,正常的想法就是在上述算法上每次取出\(k\)的节点
但加入最后一次取不出\(k\)个时,也就是第一层未满,此时从下方任意取出一个子树接在根节点的下面都会更优
因此咱们要进行一些操做
咱们插入一些额外的权值为\(0\)的叶子节点,知足\((n-1)mod(k-1) = 0\)
这是在根据上述思路作便可,由于补\(0\)后只有最下面的一次是不满的
\(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; }
这道题目背景比多,有考阅读的成分
简化版的提议就是求\(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; }