• 数据结构问题总结


    线段树 && 树状数组

    • P1908 逆序对

      这题太经典了,做法有很多,可以归并排序,可以树状数组,可以权值线段树,这里只说一下权值线段树的做法。权值线段树的作用是维护值域中每个数在序列中出现了多少次,所以其占用空间与值域很有关系。如果值域过大,我们需要离散化一下(就是排序一下,然后用二分查每个数的排名)。我们知道线段树之类的东西需要有询问或者修改操作才行,那么这个问题有什么询问和修改操作呢?可以这样想,我们每次往树中插入一个数,然后看看这个数和它前面的数能组成多少逆序对,那么修改就是插入数,查询就是看比这个数小的数现在出现了多少个,即每个数出现次数的前缀和,然后用目前已经插入的数的个数减去不大于它的数的个数,就是前面已经插入的比它大的数的个数,对于插入数,其实就是某个数出现次数+1,查询前缀和就不用说了。如果没接触过权值线段树的话,可能还是比较难想到这个做法的。在这里我贴上权值线段树的AC代码:

      PS:我这个题卡了好久,离散化和二分,甚至单点修改都静态debug了好久,不过还好一发提交就AC了。

      #include <cstdio>
      #include <cstdlib>
      #include <algorithm>
      #define ll long long
      using namespace std;
      const int N=5e5+9;
      ll a[N],q[N];
      ll f[4*N],n;
      void pushup(ll k);
      int cmp(const void *a,const void *b);
      void modify(ll left,ll right,ll pos,ll k);
      ll query(ll left,ll right,ll ql,ll qr,ll k); 
      int main(){
      	ll l,r,mid,pos,ans=0;
      	scanf("%lld",&n);
      	for(int i=1;i<=n;i++){
      		scanf("%lld",&a[i]);
      		q[i]=a[i];
      	}
      	qsort(q+1,n,sizeof(ll),cmp);
      	for(int i=1;i<=n;i++){
      		l=1;
      		r=n;
      		while(l<=r){
      			mid=(l+r)/2;
      			if(q[mid]<a[i]){ //找到离散化之后的数据 
      				l=mid+1;
      			} else {
      				pos=mid;
      				r=mid-1;
      			}
      		}
      		modify(1,n,pos,1); //pos是a[i]离散化之后的数据 
      		ans=ans+i-query(1,n,1,pos,1); //把前i个数里面小于等于pos的数据统计起来,i-cnt就是大于的个数  
      	}
      	printf("%lld
      ",ans);
      	return 0;
      }
      void pushup(ll k){
      	f[k]=f[2*k]+f[2*k+1];
      }
      ll query(ll left,ll right,ll ql,ll qr,ll k){
      	if(ql<=left && right<=qr){
      		return f[k];
      	}
      	ll mid=(left+right)/2;
      	ll ansleft=0,ansright=0;
      	if(ql<=mid){
      		ansleft=query(left,mid,ql,qr,2*k);
      	}
      	if(qr>mid){
      		ansright=query(mid+1,right,ql,qr,2*k+1);
      	}
      	return ansleft+ansright;
      }
      void modify(ll left,ll right,ll pos,ll k){ //我发现我现在不会写单点修改了!!!
      	if(pos<left || pos>right){
      		return;
      	}
      	if(left==right && left==pos){
      		f[k]++;
      		return;
      	}
      	ll mid=(left+right)/2;
      	if(pos<=mid){
      		modify(left,mid,pos,2*k);
      	} else {
      		modify(mid+1,right,pos,2*k+1);
      	}
      	pushup(k);
      }
      int cmp(const void *a,const void *b){
      	ll *p1=(ll*)a;
      	ll *p2=(ll*)b;
      	return (*p1)-(*p2);
      }
      
    • P4588 数学计算

      题目大意见原题。

      这个题目我是从线段树题单里找到的,所以这个题目肯定用线段树能做,但是怎么做呢?我们连序列都没有啊!没关系,我们可以想办法构造序列,然后建立线段树。注意到操作1是乘法,我们不难想到,如果是第i次操作是1,乘了m,那么我们可以把序列的第i个位置改成m,这样求前缀积,再取模,就是当前的模数了。又看到操作2,相当于是对乘法的撤销,但是带着取模咋撤销呢?可以这样想,撤销相当于不乘那个数了,也就是变成乘1了,所以我们只需要把对应位置改为1,然后刷新区间积的模数就好了。这个题目总体来说建模不是很难,关键在于构造出能建立线段树的序列。事实上,在没有显式序列时,我们经常会对一个全是0或者全是1或者其他数的序列建立线段树,然后通过题目叙述的修改和查询来把序列中的数改为有意义的数据。

      代码就不放出来了,因为和线段树的模板大同小异,只要能建模出来,就只需要把模板改一下能AC了。

    • P1966 火柴排队

      题目大意见原题。

      印象深刻,教训深刻的一道题目。对于一个出现数学公式的题目,我们一定不能放任着他不管,除非它根本不可化简或者计算,否则一定要做一些必要的化简工作,即便是重新组合一下顺序,把形式相同的项放在一起也很有用。比如之前做过一道题,就是如果把式子展开并且按照形式归类之后,发现可以用前缀和减少重复计算。本题虽然不是用前缀和简化计算,但是不化简的话,很难做出来本题。把待求式化简之后,我们发现:(sum a_i^{2})(sum b_i^2)都是固定的,不管怎么调换顺序都是不变的,唯一的变量是(sum a_ib_i) ,为了让待求式最小,我们需要让这个和最大。如果学过排序不等式的话,肯定知道顺序和>=乱序和>=反序和。所以为了让这个和最大,需要进行一些排序的工作,让这两个序列大小排名相同的火柴放在一起。那么如何看需要移动多少次才能实现这一点呢?可以这样想:我们先拷贝一份数组a到c中,然后对数组c排序,然后构造数组q,q[i]存储的是a中第i个元素的排名(您可能看出来了这里是在做离散化),接着,我们可以对b数组进行相同操作,即拷贝后排序,构造编号数组r[]。这时候,不难发现q和r都是1-n的一个排列,我们想做的事情就是通过对其中一个排列进行操作,得到另一个排列。假设q是1 3 2 4 5,r是1 5 4 2 3,不妨把q变成a b c d e,这样r就得变成a e d c b(rxz大佬将LCS转化为LIS问题时的思想)。如果按照字典序的话,q已经有序了,我们只需要看看需要多少次交换才能让r变得有序就好了,这就成了一个求逆序对有多少个的问题。逆序对可以权值线段树搞定,但是这里显然用mergesort更简单。为了便于理解,我可能进行了多次不必要的拷贝和映射,如果脑子比较好用,大可一步到位,一次映射搞定问题。

      代码如下,可能省去了很多不必要的拷贝和映射,不过思想是一样的:

      #include <bits/stdc++.h>
      #define ll long long
      using namespace std;
      const int N=1e5+9;
      const int mod=1e8-3;
      typedef struct{
      	ll value,pos;
      }NB;
      NB a[N],b[N],tmp[N],q[N];
      ll n,ans;
      void Mergesort(ll start,ll end,NB m[]);
      void Merge(ll start,ll mid,ll end,NB m[]);
      int main(){
      	scanf("%lld",&n);
      	for(int i=1;i<=n;i++){
      		scanf("%lld",&a[i].value);
      		a[i].pos=i;
      	}
      	for(int i=1;i<=n;i++){
      		scanf("%lld",&b[i].value);
      		b[i].pos=i;
      	}
      	Mergesort(1,n,a);
      	Mergesort(1,n,b);//离散化 
      	for(ll i=1;i<=n;i++){
      		q[a[i].pos].value=b[i].pos;
      	}
      	ans=0;
      	Mergesort(1,n,q);
      	printf("%lld
      ",ans);
      	return 0;
      }
      void Mergesort(ll start,ll end,NB m[]){
      	if(start<end){
      		ll mid=(start+end)/2;
      		Mergesort(start,mid,m);
      		Mergesort(mid+1,end,m);
      		Merge(start,mid,end,m);
      	}
      }
      void Merge(ll start,ll mid,ll end,NB m[]){
      	ll i=start,j=mid+1,k=start;
      	for(;i<=mid && j<=end;k++){
      		if(m[i].value<=m[j].value)
      			tmp[k]=m[i++];
      		else{
      			tmp[k]=m[j++];
      			ans=(ans+mid-i+1)%mod;
      		}	
      	}
      	for(;i<=mid;i++,k++)
      		tmp[k]=m[i];
      	for(;j<=end;j++,k++)
      		tmp[k]=m[j];
      	for(i=start;i<=end;i++)
      		m[i]=tmp[i];
      	return;
      }
      
    • P6584 重拳出击

      未完待续

      题目大意:有一棵树,你最开始在某个结点上,树上每个结点有点权,每次删除操作,你都可以把离你所在的结点距离不超过(k)的结点的点权都修改为0,然后你可以选择将自己移动一步或者不移动,其他节点的点权必须沿着向你的方向的简单路径转移,求需要多少次删除操作才能把树上的点的点权全部变成0?

      这道题看起来是一道数据结构题。这道题的规则花里胡哨的,我们一个一个看一下,想想怎么搞。首先,把所有离某个结点距离不超过某个数(k)的结点的点权全部变成0,不是传统的子树修改或者区间修改,直接暴力修改的话就是迭代加深,如果是把初始点记为根节点的话,其实就是层次遍历,如果想比较快的修改的话,可能考虑树剖,那样的话就得想怎么把对应的区间的编号算出来,由于刚才的层次遍历的启发,我想到一种定义(bfs) 序,然后建立线段树维护的方法(结果查了一下天哪真的有(bfs)序这种东西)。这样的话,删除的区间在(bfs)序上就比较容易求出来,一次修改复杂度是(O(log^2n))

      其次,对于换根操作(有了刚才的思考,我觉得把这个操作叫做换根并不为过),动手画了几个图之后,没有想出来怎么表示今后的修改区间。

      最后,对于移动操作,如果根不换的话,就是把儿子的权值加到父亲上,然后儿子的权值清零。

      上面的思路有很多操作并不能高效实现,可能废了。

      下面是L_C_A大佬给出的思路:我们杀死所有小怪所用的时间的最小值,取决于离我们最远的小怪有多远,想到这一点,可能会有二分的冲动。假设最远距离是(maxdist),则在([1,maxdist])中二分答案。怎样写check呢?看了看数据范围,应该需要在(O(n))的时间内完成check。考虑到我们check的时候一般都是贪心的,所以这里我们也贪心地去验证(key)回合能否杀死所有小怪。显然,如果当前的(maxdist<=k),则可以1回合杀完,否则的话,我们先杀光范围内的小怪,然后考虑移动:如果自己不移动的话,就是所有其他的小怪离自己的距离-1,如果自己移动的话,肯定是往最远的小怪的方向移动,并且最远的小怪也会向自己移动,这样就相当于最远的小怪离自己的距离-2了,那其他小怪呢?显然,自己移动的方向上的所有小怪的距离都是-2,除此之外的小怪,离自己的距离不变。如何判断哪些结点是在最大值方向的结点呢?看起来好像和子树有关系。自己走到了一个新的节点,那么那个节点的子树上的所有小怪离自己的距离就会-2。

    • 线段树单点修改板子:

      void modify(ll left,ll right,ll pos,ll val,ll k){
      	if(pos<left || pos>right ) return ; //必须要有,否则会无限递归
      	if(left==pos && pos==right){
      		f[k]=val;
      		return;
      	}
      	ll mid=(left+right)/2;
      	if(pos<=mid){ //一定按照线段树的规则
      		modify(left,mid,pos,val,2*k);
      	} else {
      		modify(mid+1,right,pos,val,2*k+1);
      	}
      	pushup(k);
      }
      
    • 牛客 数学问题

      题目大意:有一个数列,长度为n,允许你用两个不相交的长度为k的区间去覆盖数列,求覆盖的部分的数的和的最大值。

      这道题目我的想法是枚举左面那个区间,然后用(O(logn))的时间求出来它对应的右边区间的最大值。如何实现对数查询呢?我开始想的是倍增,但是不太会写。后来看了题解,发现可以线段树。如果对区间长度为k的区间的和的数组建立线段树,那么查询最大值就是单点查询了,十分简单。

      当然,这道题还有一个(dp)做法:枚举点,对于一个点,求它左边的长度为k的区间的最大值和右边长度为k的区间的最大值,动态更新答案。

      这道题的经验是:

      1. 对数查询优先考虑线段树,考虑构造相应数列来建树。
      2. 对于不带修的查询,考虑前缀和。
    • P2572 序列操作

      题目大意:有一个数列,你需要快速将某个区间内所有的数变成1或者0或者取反或者查询1的个数或者查询连续的1的个数。

      考虑前两个操作:都是覆盖性的操作,性质相同,所以如果只维护这两个的话是完全没有问题的。

      考虑第三个操作:如果只有取反的话,也好说,但是取反操作和前两种操作的性质明显不一样,所以需要再开一个标记来做。模拟一下可以发现,在对一个极大区间标记取反之前,我们要把前两种覆盖性的操作的标记下放然后清空,不然的话之后就不知道是先取反还是先覆盖的了。然后根据老师的提示,注意到如果有一个区间有取反标记,然后又被染色的话,就不用取反了。

      考虑第四个操作:其实还好,我们可以让线段树的结点的值就存储该结点表示的区间的1的个数,然后根据那些标记进行修改就好了。

      考虑第五个操作:最多多少个连续的1,显然单个点很好说,如果是区间的话,我们要知道包含区间左端点的最多连续的1的个数是多少(记为(cnt_1)),包含右端点的最多连续的1的个数是多少(记为(cnt_2)),除此之外最多连续的1的个数是多少(记为(cnt_3)),然后在合并的时候假设左边的区间叫x,右边的区间叫y,则区间合并之后的结果就是(max(xcnt_1,xcnt_3,xcnt_2+ycnt_1,ycnt_2,ycnt_3)) 。注意,在查询的时候,应该是左边区间的查询结果、右边区间的查询结果以及左区间右连续+右区间左连续的结果的最大值。还有一点是,左区间右连续的结果和右区间左连续的结果要保证在([ql,qr])的范围内,也就是说得取一个min,详见代码注释。

      这道题目的经验是:对于覆盖性操作标记和其他类型标记,在打覆盖性标记时可能会让其他标记失效,类似地,在已经有覆盖性标记时又打其他标记,覆盖性标记也会发生变化。

      代码如下(很长):

      #include <bits/stdc++.h>
      #define ll long long
      using namespace std;
      const int N=1e5+9;
      const int M=4e5+9;
      typedef struct{
      	ll cnt; //这个区间有多少个数 
      	ll sum;
      	ll concrete_one,concrete_zero;
      	ll left_zero_cnt,right_zero_cnt,mid_zero_cnt;
      	ll left_one_cnt,right_one_cnt,mid_one_cnt;
      	ll color,reverse; //color初始化为-1 
      }SMT;
      SMT f[4*N];
      ll n,m,a[N];
      void pushup(ll k);
      void build(ll left,ll right,ll k);
      void pushdown(ll left,ll right,ll k);
      void modify0(ll left,ll right,ll ql,ll qr,ll k);
      void modify1(ll left,ll right,ll ql,ll qr,ll k);
      void modify2(ll left,ll right,ll ql,ll qr,ll k);
      ll query3(ll left,ll right,ll ql,ll qr,ll k);
      ll query4(ll left,ll right,ll ql,ll qr,ll k);
      int main(){
      	ll op,l,r;
      	scanf("%lld %lld",&n,&m);
      	for(int i=1;i<=n;i++){
      		scanf("%lld",&a[i]);
      	}
      	for(ll i=1;i<=M;i++){
      		f[i].color=-1;
      	}
      	build(1,n,1);
      	while(m--){
      		scanf("%lld %lld %lld",&op,&l,&r);
      		if(op==0){
      			modify0(1,n,l+1,r+1,1);
      		} else if(op==1){
      			modify1(1,n,l+1,r+1,1);
      		} else if(op==2){
      			modify2(1,n,l+1,r+1,1);
      		} else if(op==3){
      			printf("%lld
      ",query3(1,n,l+1,r+1,1));
      		} else {
      			printf("%lld
      ",query4(1,n,l+1,r+1,1));
      		}
      	}
      	return 0;
      } 
      void modify1(ll left,ll right,ll ql,ll qr,ll k){
      	if(ql<=left && right<=qr){
      		f[k].color=1;
      		f[k].concrete_one=f[k].cnt;
      		f[k].left_one_cnt=f[k].cnt;
      		f[k].mid_one_cnt=f[k].cnt;
      		f[k].right_one_cnt=f[k].cnt;
      		f[k].left_zero_cnt=0;
      		f[k].mid_zero_cnt=0;
      		f[k].right_zero_cnt=0;
      		f[k].concrete_zero=0;
      		f[k].reverse=0;
      		f[k].sum=f[k].cnt;
      		return;
      	}
      	pushdown(left,right,k);
      	ll mid=(left+right)/2;
      	if(ql<=mid){
      		modify1(left,mid,ql,qr,2*k);
      	}
      	if(qr>mid){
      		modify1(mid+1,right,ql,qr,2*k+1);
      	}
      	pushup(k);
      } 
      void modify2(ll left,ll right,ll ql,ll qr,ll k){
      	if(ql<=left && right<=qr){
      		if(f[k].color!=-1){ //如果这个区间已经有了染色标记,则把染色标记翻转 
      			f[k].color^=1;
      		} else { //如果没有染色标记,则翻转标记加一 
      			f[k].reverse^=1; 
      		}
      		swap(f[k].concrete_one,f[k].concrete_zero);
      		swap(f[k].left_one_cnt,f[k].left_zero_cnt);
      		swap(f[k].mid_one_cnt,f[k].mid_zero_cnt);
      		swap(f[k].right_one_cnt,f[k].right_zero_cnt);
      		f[k].sum=f[k].cnt-f[k].sum;
      		return;
      	}
      	pushdown(left,right,k);
      	ll mid=(left+right)/2;
      	if(ql<=mid){
      		modify2(left,mid,ql,qr,2*k);
      	}
      	if(qr>mid){
      		modify2(mid+1,right,ql,qr,2*k+1);
      	}
      	pushup(k);
      }
      void modify0(ll left,ll right,ll ql,ll qr,ll k){
      	if(ql<=left && right<=qr){
      		f[k].color=0;
      		f[k].concrete_one=0;
      		f[k].left_one_cnt=0;
      		f[k].mid_one_cnt=0;
      		f[k].right_one_cnt=0;
      		f[k].left_zero_cnt=f[k].cnt;
      		f[k].mid_zero_cnt=f[k].cnt;
      		f[k].right_zero_cnt=f[k].cnt;
      		f[k].concrete_zero=f[k].cnt; 
      		f[k].reverse=0;
      		f[k].sum=0;
      		return;
      	}
      	pushdown(left,right,k);
      	ll mid=(left+right)/2;
      	if(ql<=mid){
      		modify0(left,mid,ql,qr,2*k);
      	}
      	if(qr>mid){
      		modify0(mid+1,right,ql,qr,2*k+1);
      	}
      	pushup(k);
      }
      ll query4(ll left,ll right,ll ql,ll qr,ll k){
      	if(ql<=left && right<=qr){
      		return f[k].concrete_one;
      	}
      	pushdown(left,right,k);
      	ll mid=(left+right)/2;
      	ll ans=0;
      	if(ql<=mid){
      		ans=query4(left,mid,ql,qr,2*k);
      	}
      	if(qr>mid){
      		ans=max(ans,query4(mid+1,right,ql,qr,2*k+1));
      	}
      	if(ql<=mid && qr>mid)
      		ans=max(ans,min(f[2*k].right_one_cnt,mid-ql+1)+min(f[2*k+1].left_one_cnt,qr-mid)); //很关键,防止越界
      	return ans;
      }
      ll query3(ll left,ll right,ll ql,ll qr,ll k){
      	if(ql<=left && right<=qr){
      		return f[k].sum;
      	}
      	pushdown(left,right,k);
      	ll mid=(left+right)/2;
      	ll ret=0;
      	if(ql<=mid){
      		ret+=query3(left,mid,ql,qr,2*k);
      	}
      	if(qr>mid){
      		ret+=query3(mid+1,right,ql,qr,2*k+1);
      	}
      	return ret;
      }
      void pushdown(ll left,ll right,ll k){
      	if(f[k].color!=-1){
      		f[2*k].color=f[k].color;
      		f[2*k+1].color=f[k].color;
      		f[2*k].reverse=0; //取反失效 
      		f[2*k+1].reverse=0;
      		if(f[k].color==0){
      			f[2*k].sum=0;
      			f[2*k].left_one_cnt=0;
      			f[2*k].mid_one_cnt=0;
      			f[2*k].right_one_cnt=0;
      			f[2*k].left_zero_cnt=f[2*k].cnt;
      			f[2*k].mid_zero_cnt=f[2*k].cnt;
      			f[2*k].right_zero_cnt=f[2*k].cnt;
      			f[2*k].concrete_one=0;
      			f[2*k].concrete_zero=f[2*k].cnt;
      			f[2*k+1].sum=0;
      			f[2*k+1].left_one_cnt=0;
      			f[2*k+1].mid_one_cnt=0;
      			f[2*k+1].right_one_cnt=0;
      			f[2*k+1].left_zero_cnt=f[2*k+1].cnt;
      			f[2*k+1].mid_zero_cnt=f[2*k+1].cnt;
      			f[2*k+1].right_zero_cnt=f[2*k+1].cnt;
      			f[2*k+1].concrete_one=0;
      			f[2*k+1].concrete_zero=f[2*k+1].cnt;
      		} else {
      			f[2*k].sum=f[2*k].cnt;
      			f[2*k].left_one_cnt=f[2*k].cnt;
      			f[2*k].mid_one_cnt=f[2*k].cnt;
      			f[2*k].right_one_cnt=f[2*k].cnt;
      			f[2*k].left_zero_cnt=0;
      			f[2*k].mid_zero_cnt=0;
      			f[2*k].right_zero_cnt=0;
      			f[2*k].concrete_one=f[2*k].cnt;
      			f[2*k].concrete_zero=0;
      			f[2*k+1].sum=f[2*k+1].cnt;
      			f[2*k+1].left_one_cnt=f[2*k+1].cnt;
      			f[2*k+1].mid_one_cnt=f[2*k+1].cnt;
      			f[2*k+1].right_one_cnt=f[2*k+1].cnt;
      			f[2*k+1].left_zero_cnt=0;
      			f[2*k+1].mid_zero_cnt=0;
      			f[2*k+1].right_zero_cnt=0;
      			f[2*k+1].concrete_one=f[2*k+1].cnt;
      			f[2*k+1].concrete_zero=0;
      		}
      		f[k].color=-1;
      	}
      	if(f[k].reverse!=0){
      		f[2*k].sum=f[2*k].cnt-f[2*k].sum;
      		swap(f[2*k].left_one_cnt,f[2*k].left_zero_cnt);
      		swap(f[2*k].mid_one_cnt,f[2*k].mid_zero_cnt);
      		swap(f[2*k].right_one_cnt,f[2*k].right_zero_cnt);
      		swap(f[2*k].concrete_one,f[2*k].concrete_zero);
      		
      		f[2*k+1].sum=f[2*k+1].cnt-f[2*k+1].sum;
      		swap(f[2*k+1].left_one_cnt,f[2*k+1].left_zero_cnt);
      		swap(f[2*k+1].mid_one_cnt,f[2*k+1].mid_zero_cnt);
      		swap(f[2*k+1].right_one_cnt,f[2*k+1].right_zero_cnt);
      		swap(f[2*k+1].concrete_one,f[2*k+1].concrete_zero);
      		
      		f[2*k].reverse^=f[k].reverse;
      		f[2*k+1].reverse^=f[k].reverse;
      		f[k].reverse=0;
      	}
      	//下面应该是pushup的内容 
      //	f[k].concrete_zero=max(f[2*k].left_zero_cnt,max(f[2*k].mid_zero_cnt,max(f[2*k].right_zero_cnt+f[2*k+1].left_zero_cnt,max(f[2*k+1].mid_zero_cnt,f[2*k+1].right_zero_cnt))));
      //	f[k].concrete_one=max(f[2*k].left_one_cnt,max(f[2*k].mid_one_cnt,max(f[2*k].right_one_cnt+f[2*k+1].left_one_cnt,max(f[2*k+1].mid_one_cnt,f[2*k+1].right_one_cnt))));
      }
      inline void pushup(ll k){
      	if(f[2*k].left_one_cnt==f[2*k].cnt){
      		f[k].left_one_cnt=f[2*k].left_one_cnt+f[2*k+1].left_one_cnt; //左区间内全是1 
      	} else {
      		f[k].left_one_cnt=f[2*k].left_one_cnt; 
      	}
      	if(f[2*k].left_zero_cnt==f[2*k].cnt){
      		f[k].left_zero_cnt=f[2*k].left_zero_cnt+f[2*k+1].left_zero_cnt;
      	} else {
      		f[k].left_zero_cnt=f[2*k].left_zero_cnt;
      	}
      	f[k].mid_one_cnt=max(f[2*k].right_one_cnt+f[2*k+1].left_one_cnt,max(f[2*k].mid_one_cnt,f[2*k+1].mid_one_cnt)); //可能有点问题 
      	f[k].mid_zero_cnt=max(f[2*k].right_zero_cnt+f[2*k+1].left_zero_cnt,max(f[2*k].mid_zero_cnt,f[2*k+1].mid_zero_cnt));
      	if(f[2*k+1].right_one_cnt==f[2*k+1].cnt){
      		f[k].right_one_cnt=f[2*k+1].right_one_cnt+f[2*k].right_one_cnt;
      	} else {
      		f[k].right_one_cnt=f[2*k+1].right_one_cnt;
      	}
      	if(f[2*k+1].right_zero_cnt==f[2*k+1].cnt){
      		f[k].right_zero_cnt=f[2*k+1].right_zero_cnt+f[2*k].right_zero_cnt;
      	} else {
      		f[k].right_zero_cnt=f[2*k+1].right_zero_cnt;
      	}
      	f[k].sum=f[2*k].sum+f[2*k+1].sum; //总共的1的个数 
      	f[k].concrete_zero=max(f[2*k].left_zero_cnt,max(f[2*k].mid_zero_cnt,max(f[2*k].right_zero_cnt+f[2*k+1].left_zero_cnt,max(f[2*k+1].mid_zero_cnt,f[2*k+1].right_zero_cnt))));
      	f[k].concrete_one=max(f[2*k].left_one_cnt,max(f[2*k].mid_one_cnt,max(f[2*k].right_one_cnt+f[2*k+1].left_one_cnt,max(f[2*k+1].mid_one_cnt,f[2*k+1].right_one_cnt))));
      	f[k].cnt=f[2*k].cnt+f[2*k+1].cnt;
      }
      void build(ll left,ll right,ll k){
      	if(left==right){
      		f[k].cnt=1;
      		f[k].sum=a[left];
      		f[k].concrete_one=a[left];
      		f[k].concrete_zero=1-a[left];
      		f[k].left_one_cnt=a[left];
      		f[k].left_zero_cnt=1-a[left];
      		f[k].mid_one_cnt=a[left];
      		f[k].mid_zero_cnt=1-a[left];
      		f[k].right_one_cnt=a[left];
      		f[k].right_zero_cnt=1-a[left];
      		return; 
      	}
      	ll mid=(left+right)/2;
      	build(left,mid,2*k);
      	build(mid+1,right,2*k+1);
      	pushup(k);
      }
      

    树链剖分

    • P4427 求和

      题目大意:有一棵树,每个点都有点权,查询树上一条路径上的点的权值的k次方和,其中1<=k<=50

      这个题我是从树剖的题单找过去的,所以我自然往树剖上想的。如果是查询某确定次方和的话,就是树剖裸题,注意到本题k范围比较小,所以我们可以预处理出每个点的点权的1-50次方,然后查询的时候直接用树剖转换为区间问题,用树状数组查相应的前缀和,作差就是结果了。对于本题来说,有一个坑点:由于要取模,所以最后作差可能出现负数,所以我们应该作差之后+mod,然后再%mod,这样才保证了结果的正确性,不这么做会爆零。另外,这个题目数据是3e5,我们的算法复杂度有两个log,所以需要读写优化+内联进行卡常才能通过。

      代码不放了,基本上树剖的代码长得都差不多,主要是题目建模(但这是个裸题)。

      另外,这个题正解并不是树剖,以后有机会会补上正解。

      做了一些题之后,明白了,这种有结合律并且还不带修改的信息,完全可以倍增维护,复杂度降低一个(log)

    • P3038 边权树剖

      这是一道模板题,让用树剖维护树上的边权信息。

      这道题做法是把边权转化为点权,一种转化方法是把边权转化为它较深的端点的点权。路径修改边权时,即修改点权,但对于路径来说,lca的点权不能修改,因为显然lca的点权代表的边权不在路径上。在查询边权时,即查询边的较深的端点的点权就好了。捎带说一句,之前我是没用树剖求过lca的,但是这里我知道了,树剖两个点最后跳到同一条重链的时候,深度小的那个就是原来那两个点的lca。

      代码不放了,几乎就是树剖的模板,只改了上述几个地方。

    • P3979 换根树剖

      题目大意:一棵树,你需要修改某条路径上的点权为同一个数,可以指定某个点为根,可以查询以某个点为根的子树中的点权的最小值。

      换根我们现在是第二次碰到了,思考方法主要是考虑使用新根和使用老根之间的变与不变。

      首先,修改操作不管换不换根都是不变的,因为他是路径修改而不是子树修改。

      换根之后,一种想法是,把信息都修改了,但是太慢了。

      其实不用改它们的信息,我们最开始默认按照1号为根进行树剖预处理,然后在查询的时候,根据查询的点和新的根的关系,确定查询的区间就好了。

      根据要查询的结点和(newroot)的逻辑关系来分类:

      第一种情况,更简洁的表述是(i)不是(newroot)的祖先,这个是容易判断的,可以利用树剖(lca),看(i和newroot)的最近公共祖先。注意先判断第四种情况,因为那时候(lca)和这种情况是一样的。

      (i)(newroot)本尊,则直接按照1号根查询全局最小值就好了。

      (i)在以(newroot) 为根的子树中,则换根前后查询结果不变。

      (i)(newroot)(oldroot)这条链上,则可以画图看一下,按照以(newroot)为根,适当把边进行一下旋转,能够发现,(i)的查询范围,就是整个树-(i)的包含(newroot) 那棵子树。记那棵子树的根为(y),则(y)是换根之前(i)的儿子,且(y)的所有后代里面有(newroot) 。现在的问题是如何快速查询这个补集的信息呢?思考一下发现,(y)及其子树的dfs序的区间是([v[y].id,v[y].id+v[y].size-1]) ,所以我们只需要查询([1,v[y].id-1])([v[y].id+v[y].size,n]) 就可以了。

      本题主要在于如何合理利用树剖,代码的主要部分还是树剖的模板,所以就不放了。关于代码细节,这道题我的(pushdown)函数写错了,很致命,要知道线段树里面最核心的部分就是(pushdown)了,所以一定要谨慎思考;另外树剖预处理部分的dfs也写错了,不过静态debug发现了;然后我写的树上倍增居然也错了,好像是移位数太多了。不得不说这个题数据很水,犯了三个致命错误,居然可以得到90分。

    • P4092 树

      题目大意:有一棵树,最开始树上的点都没有标记,我们可以对树进行两种操作:对一个结点打标记;查询离某个结点最近的打标记的祖先结点。

      我们先考虑链上的做法:一个显然的暴力是每次询问的时候就直接往前找,但会超时。我们考虑维护区间的信息来加速,即在标记了一个结点之后,储存某个区间内的一些额外信息,比如某个区间([l,r])内的深度最深的打标记的点。考虑一个经典的长度为8的链的例子,建立线段树,假设对5号点进行标记,那么([1,8],[5,8],[5,6],[5,5])这四个区间维护的区间内打标记最深点的深度都有可能被刷新,这个可以在进行单点修改之后回溯时用(pushup)函数来实现。查询的话,比如查询7号点,那么可以直接查区间([1,7])的那个信息,只要在查询时对各个极大区间取(max)就好了。这样的话,树上的也很明了了:修改一个点时就用(dfs)序来刷新信息,查询一个点时,即查询该点到根这条路径上的最深的打标记的点。问题解决!

    • P6584 打怪

      未完待续

      题目大意:有一棵树,你最开始在某个结点上,树上每个结点有点权,每次删除操作,你都可以把离你所在的结点距离不超过(k)的结点的点权都修改为0,然后你可以选择将自己移动一步或者不移动,其他节点的点权必须沿着向你的方向的简单路径转移,求需要多少次删除操作才能把树上的点的点权全部变成0?

      这道题看起来是一道数据结构题。这道题的规则花里胡哨的,我们一个一个看一下,想想怎么搞。首先,把所有离某个结点距离不超过某个数(k)的结点的点权全部变成0,不是传统的子树修改或者区间修改,直接暴力修改的话就是迭代加深,如果是把初始点记为根节点的话,其实就是层次遍历,如果想比较快的修改的话,可能考虑树剖,那样的话就得想怎么把对应的区间的编号算出来,由于刚才的层次遍历的启发,我想到一种定义(bfs) 序,然后建立线段树维护的方法(结果查了一下天哪真的有(bfs)序这种东西)。这样的话,删除的区间在(bfs)序上就比较容易求出来,一次修改复杂度是(O(log^2n))

      其次,对于换根操作(有了刚才的思考,我觉得把这个操作叫做换根并不为过),动手画了几个图之后,没有想出来怎么表示今后的修改区间。

      最后,对于移动操作,如果根不换的话,就是把儿子的权值加到父亲上,然后儿子的权值清零。

    莫队算法

    • P1972 HH的项链

      题目大意:给定一个数列,给定查询区间[l,r],求这段区间内有多少不一样的数。

      虽然这个题正解不是莫队,离线也很容易被卡,但是本人感觉这个题可以作为一个莫队算法的入门题目。莫队算法步骤大概是:分块,询问排序,按新顺序动态调整区间与答案进行回答,最后按照原序输出询问结果。关于莫队算法究竟如何操作,我已经写在笔记本上了,下面就只贴上代码了。

      //TLE 58pts
      #include <cstdio>
      #include <cstdlib>
      #include <algorithm>
      #include <cmath>
      #define ll long long
      using namespace std;
      typedef struct{
      	ll l,r,order,blocknum;
      }Query;
      const int N=1e6+9;
      Query q[N];
      ll n,m,a[N],blocksize,ans[N],cnt[N],now;
      void add(ll p);
      void sub(ll p);
      int cmp(const void *a,const void *b);
      int main(){
      	ll left=1,right=0;
      	scanf("%lld",&n);
      	for(int i=1;i<=n;i++){
      		scanf("%lld",&a[i]);
      	}
      	scanf("%lld",&m);
      	blocksize=sqrt(n);
      	for(int i=1;i<=m;i++){
      		scanf("%lld %lld",&q[i].l,&q[i].r);
      		q[i].order=i;
      		q[i].blocknum=q[i].l/blocksize;
      	}
      	qsort(q+1,m,sizeof(Query),cmp);
      //	for(int i=1;i<=m;i++){
      //		printf("%lld %lld
      ",q[i].l,q[i].r);
      //	}
      	for(int i=1;i<=m;i++){
      		while(left<q[i].l){
      			sub(left);
      			left++;
      		}
      		while(left>q[i].l){
      			left--;
      			add(left);
      		}
      		while(right<q[i].r){
      			right++;
      			add(right);
      		}
      		while(right>q[i].r){
      			sub(right);
      			right--;
      		}
      		ans[q[i].order]=now;
      	}
      	for(int i=1;i<=m;i++){
      		printf("%lld
      ",ans[i]);
      	}
      	return 0;
      }
      void add(ll p){
      	if(cnt[a[p]]==0){
      		now++;
      	}
      	cnt[a[p]]++;
      }
      void sub(ll p){
      	cnt[a[p]]--;
      	if(cnt[a[p]]==0){
      		now--;
      	}
      }
      int cmp(const void *a,const void *b){
      	Query *p1=(Query*)a;
      	Query *p2=(Query*)b;
      	if(p1->blocknum==p2->blocknum){
      		return p1->r-p2->r;
      	}
      	return p1->blocknum-p2->blocknum;
      }
      

    平衡树

    dbp我现在只会写AVL树,并且只写过了普通平衡树。等到我学会了splay,写过了文艺和二逼之后,再回来补上。

    Trie树

    • P4551 最长异或路径

      题意:在树中找一条路径,使得这条路径的边权的异或和最大。

      预处理每个节点到根的异或和是套路。然后,考虑枚举两个点,(O(1))查询其异或和,这样的话总的复杂度是(O(n^2))。为了提高效率,我们要利用数据结构的力量。根据经验,解决最大异或值的问题的时候一般是从高位往下找,且一般会使用trie树来实现这个贪心。所以我们考虑用trie做这个题。

      为了保证结果正确,我们建trie树的时候,要让所有的数的二进制表达长度一样。然后就是把trie建出来,然后贪心就好了。

      代码如下:

      #include <bits/stdc++.h>
      #define ll long long
      #define INF 999999999999
      using namespace std;
      const int N=1e5+9;
      typedef struct{
      	ll to,nxt,weight;
      }Edge;
      typedef struct ss{
      	ll value;
      	struct ss *child[3];
      	bool isend;
      	ll seq;
      }Node;
      typedef Node* Nodeptr;
      Edge edge[2*N];
      ll head[N],cnt,n,val[N],component[N];
      void add(ll x,ll y,ll z);
      ll query(ll v,Nodeptr root);
      void dfs(ll now,ll fa,ll sum);
      Nodeptr insert(ll v,Nodeptr root,ll seq);
      int main(){
      	ll x,y,z,now,ans=0;
      	Nodeptr root=NULL;
      	scanf("%lld",&n);
      	for(int i=0;i<=n;i++){
      		head[i]=-1;
      	}
      	for(int i=1;i<n;i++){
      		scanf("%lld %lld %lld",&x,&y,&z);
      		add(x,y,z);
      		add(y,x,z);
      	}
      	dfs(1,0,0);
      	for(int i=1;i<=n;i++){
      		root=insert(val[i],root,i);
      	}
      	for(int i=1;i<=n;i++){
      		now=query(val[i],root);
      		ans=max(ans,now);
      	}
      	printf("%lld
      ",ans);
      	return 0;
      } 
      ll query(ll v,Nodeptr root){
      	ll tmp=v,cnt=0,ret=0;
      	Nodeptr p;
      	p=root;
      	for(int i=0;i<=33;i++){
      		component[i]=0;
      	}
      	while(tmp){
      		if(tmp&1){
      			component[cnt++]=1;
      		} else {
      			component[cnt++]=0;
      		}
      		tmp=tmp>>1;
      	}
      	for(int i=33;i>=0;i--){
      		if(p->child[component[i]^1]!=NULL){
      			p=p->child[component[i]^1];
      		} else {
      			p=p->child[component[i]];
      		}
      	}
      	if(p->isend){
      		ret=v^(val[p->seq]);
      	}
      	return ret;
      }
      Nodeptr insert(ll v,Nodeptr root,ll seq){
      	ll tmp=v,cnt=0;
      	Nodeptr p;
      	if(root==NULL){
      		root=(Nodeptr)malloc(sizeof(Node));
      		root->value=0;
      		root->isend=false;
      		for(int i=0;i<3;i++){
      			root->child[i]=NULL;
      		}
      	}
      	p=root;
      	for(int i=0;i<=33;i++){
      		component[i]=0;
      	}
      	while(tmp){
      		if(tmp&1){
      			component[cnt++]=1;
      		} else {
      			component[cnt++]=0;
      		}
      		tmp=tmp>>1;
      	}
      	for(int i=33;i>=0;i--){ //从高位到低位开始插入,长度对齐 
      		if(p->child[component[i]]!=NULL){
      			p=p->child[component[i]]; 
      		} else {
      			p->child[component[i]]=(Nodeptr)malloc(sizeof(Node));
      			p=p->child[component[i]];
      			p->value=component[i];
      			p->isend=false;
      			for(int j=0;j<3;j++){
      				p->child[j]=NULL;
      			}
      		}
      	}
      	p->isend=true;
      	p->seq=seq;
      	return root;
      }
      void dfs(ll now,ll fa,ll sum){
      	val[now]=sum;
      	for(ll i=head[now];i>=0;i=edge[i].nxt){
      		if(edge[i].to!=fa){
      			dfs(edge[i].to,now,sum^edge[i].weight);
      		}
      	}
      }
      void add(ll x,ll y,ll z){
      	edge[cnt].to=y;
      	edge[cnt].weight=z;
      	edge[cnt].nxt=head[x];
      	head[x]=cnt++;
      }
      

    并查集

    • P1955 程序自动分析

      题目大意:有一些变量相等或者不等的限制条件,现在判定这些条件是否能同时满足。

      相等是一种等价关系,所以我们可以考虑用并查集来表示这种二元关系。不等,意味着两个元素不应该在一个集合中,我们只需要调用together方法进行判定就好了。注意,我们需要先把所有的相等关系都弄好之后,才能处理不等关系,否则有可能出现最开始两个元素不在一个集合,但是后来又被合并的情况。

      本题多组数据,注意清零。为了无bug,建议完全清零。

      #include <bits/stdc++.h>
      #define ll long long
      using namespace std;
      const int N=1e6+9;
      unordered_map<int,int> table; //假的hash表 
      int f[N],t,n,cnt;
      typedef struct{
      	int i,j,e;
      }Node;
      Node a[N];
      int find(int x);
      void un(int x,int y);
      bool together(int x,int y);
      int main(){
      	scanf("%d",&t);
      	while(t--){
      		bool flag=true;
      		table.erase(table.begin(),table.end()); //先清空map
      		cnt=0;
      		scanf("%d",&n); 
      		for(int k=1;k<=n;k++){
      			f[k]=k;
      		}
      		for(int k=1;k<=n;k++){
      			scanf("%d %d %d",&a[k].i,&a[k].j,&a[k].e);
      		}
      		sort(a+1,a+n+1); //1放在前面,0放在后面 
      		for(int k=1;k<=n;k++){
      			if(table.find(a[k].i)==table.end()){
      				table[a[k].i]=++cnt;
      			}
      			if(table.find(a[k].j)==table.end()){
      				table[a[k].j]=++cnt;
      			}
      			if(a[k].e==1){
      				un(table[a[k].i],table[a[k].j]);
      			} else {
      				if(together(table[a[k].i],table[a[k].j])){
      					flag=false;
      				}
      			}
      		}
      		if(flag){
      			printf("YES
      ");
      		} else {
      			printf("NO
      ");
      		}
      	}
      	return 0;
      }
      inline bool operator <(const Node &a,const Node &b){
      	if(a.e>b.e){
      		return true;
      	} else {
      		return false;
      	} 
      }
      inline bool together(int x,int y){
      	return find(x)==find(y);
      }
      int find(int x){
      	if(x==f[x]) return x;
      	return f[x]=find(f[x]); 
      }
      void un(int x,int y){
      	x=find(x);
      	y=find(y);
      	f[x]=y;
      }
      
  • 相关阅读:
    Leetcode 92. Reverse Linked List II
    Leetcode 206. Reverse Linked List
    Leetcode 763. Partition Labels
    Leetcode 746. Min Cost Climbing Stairs
    Leetcode 759. Employee Free Time
    Leetcode 763. Partition Labels
    搭建数据仓库第09篇:物理建模
    Python进阶篇:Socket多线程
    Python进阶篇:文件系统的操作
    搭建数据仓库第08篇:逻辑建模–5–维度建模核心之一致性维度2
  • 原文地址:https://www.cnblogs.com/BUAA-Wander/p/13311305.html
Copyright © 2020-2023  润新知