线段树 && 树状数组
-
这题太经典了,做法有很多,可以归并排序,可以树状数组,可以权值线段树,这里只说一下权值线段树的做法。权值线段树的作用是维护值域中每个数在序列中出现了多少次,所以其占用空间与值域很有关系。如果值域过大,我们需要离散化一下(就是排序一下,然后用二分查每个数的排名)。我们知道线段树之类的东西需要有询问或者修改操作才行,那么这个问题有什么询问和修改操作呢?可以这样想,我们每次往树中插入一个数,然后看看这个数和它前面的数能组成多少逆序对,那么修改就是插入数,查询就是看比这个数小的数现在出现了多少个,即每个数出现次数的前缀和,然后用目前已经插入的数的个数减去不大于它的数的个数,就是前面已经插入的比它大的数的个数,对于插入数,其实就是某个数出现次数+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); }
-
题目大意见原题。
这个题目我是从线段树题单里找到的,所以这个题目肯定用线段树能做,但是怎么做呢?我们连序列都没有啊!没关系,我们可以想办法构造序列,然后建立线段树。注意到操作1是乘法,我们不难想到,如果是第i次操作是1,乘了m,那么我们可以把序列的第i个位置改成m,这样求前缀积,再取模,就是当前的模数了。又看到操作2,相当于是对乘法的撤销,但是带着取模咋撤销呢?可以这样想,撤销相当于不乘那个数了,也就是变成乘1了,所以我们只需要把对应位置改为1,然后刷新区间积的模数就好了。这个题目总体来说建模不是很难,关键在于构造出能建立线段树的序列。事实上,在没有显式序列时,我们经常会对一个全是0或者全是1或者其他数的序列建立线段树,然后通过题目叙述的修改和查询来把序列中的数改为有意义的数据。
代码就不放出来了,因为和线段树的模板大同小异,只要能建模出来,就只需要把模板改一下能AC了。
-
题目大意见原题。
印象深刻,教训深刻的一道题目。对于一个出现数学公式的题目,我们一定不能放任着他不管,除非它根本不可化简或者计算,否则一定要做一些必要的化简工作,即便是重新组合一下顺序,把形式相同的项放在一起也很有用。比如之前做过一道题,就是如果把式子展开并且按照形式归类之后,发现可以用前缀和减少重复计算。本题虽然不是用前缀和简化计算,但是不化简的话,很难做出来本题。把待求式化简之后,我们发现:(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; }
-
未完待续
题目大意:有一棵树,你最开始在某个结点上,树上每个结点有点权,每次删除操作,你都可以把离你所在的结点距离不超过(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或者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); }
树链剖分
-
题目大意:有一棵树,每个点都有点权,查询树上一条路径上的点的权值的k次方和,其中1<=k<=50
这个题我是从树剖的题单找过去的,所以我自然往树剖上想的。如果是查询某确定次方和的话,就是树剖裸题,注意到本题k范围比较小,所以我们可以预处理出每个点的点权的1-50次方,然后查询的时候直接用树剖转换为区间问题,用树状数组查相应的前缀和,作差就是结果了。对于本题来说,有一个坑点:由于要取模,所以最后作差可能出现负数,所以我们应该作差之后+mod,然后再%mod,这样才保证了结果的正确性,不这么做会爆零。另外,这个题目数据是3e5,我们的算法复杂度有两个log,所以需要读写优化+内联进行卡常才能通过。
代码不放了,基本上树剖的代码长得都差不多,主要是题目建模(但这是个裸题)。
另外,这个题正解并不是树剖,以后有机会会补上正解。
做了一些题之后,明白了,这种有结合律并且还不带修改的信息,完全可以倍增维护,复杂度降低一个(log)。
-
这是一道模板题,让用树剖维护树上的边权信息。
这道题做法是把边权转化为点权,一种转化方法是把边权转化为它较深的端点的点权。路径修改边权时,即修改点权,但对于路径来说,lca的点权不能修改,因为显然lca的点权代表的边权不在路径上。在查询边权时,即查询边的较深的端点的点权就好了。捎带说一句,之前我是没用树剖求过lca的,但是这里我知道了,树剖两个点最后跳到同一条重链的时候,深度小的那个就是原来那两个点的lca。
代码不放了,几乎就是树剖的模板,只改了上述几个地方。
-
题目大意:一棵树,你需要修改某条路径上的点权为同一个数,可以指定某个点为根,可以查询以某个点为根的子树中的点权的最小值。
换根我们现在是第二次碰到了,思考方法主要是考虑使用新根和使用老根之间的变与不变。
首先,修改操作不管换不换根都是不变的,因为他是路径修改而不是子树修改。
换根之后,一种想法是,把信息都修改了,但是太慢了。
其实不用改它们的信息,我们最开始默认按照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分。
-
题目大意:有一棵树,最开始树上的点都没有标记,我们可以对树进行两种操作:对一个结点打标记;查询离某个结点最近的打标记的祖先结点。
我们先考虑链上的做法:一个显然的暴力是每次询问的时候就直接往前找,但会超时。我们考虑维护区间的信息来加速,即在标记了一个结点之后,储存某个区间内的一些额外信息,比如某个区间([l,r])内的深度最深的打标记的点。考虑一个经典的长度为8的链的例子,建立线段树,假设对5号点进行标记,那么([1,8],[5,8],[5,6],[5,5])这四个区间维护的区间内打标记最深点的深度都有可能被刷新,这个可以在进行单点修改之后回溯时用(pushup)函数来实现。查询的话,比如查询7号点,那么可以直接查区间([1,7])的那个信息,只要在查询时对各个极大区间取(max)就好了。这样的话,树上的也很明了了:修改一个点时就用(dfs)序来刷新信息,查询一个点时,即查询该点到根这条路径上的最深的打标记的点。问题解决!
-
未完待续
题目大意:有一棵树,你最开始在某个结点上,树上每个结点有点权,每次删除操作,你都可以把离你所在的结点距离不超过(k)的结点的点权都修改为0,然后你可以选择将自己移动一步或者不移动,其他节点的点权必须沿着向你的方向的简单路径转移,求需要多少次删除操作才能把树上的点的点权全部变成0?
这道题看起来是一道数据结构题。这道题的规则花里胡哨的,我们一个一个看一下,想想怎么搞。首先,把所有离某个结点距离不超过某个数(k)的结点的点权全部变成0,不是传统的子树修改或者区间修改,直接暴力修改的话就是迭代加深,如果是把初始点记为根节点的话,其实就是层次遍历,如果想比较快的修改的话,可能考虑树剖,那样的话就得想怎么把对应的区间的编号算出来,由于刚才的层次遍历的启发,我想到一种定义(bfs) 序,然后建立线段树维护的方法(结果查了一下天哪真的有(bfs)序这种东西)。这样的话,删除的区间在(bfs)序上就比较容易求出来,一次修改复杂度是(O(log^2n)) 。
其次,对于换根操作(有了刚才的思考,我觉得把这个操作叫做换根并不为过),动手画了几个图之后,没有想出来怎么表示今后的修改区间。
最后,对于移动操作,如果根不换的话,就是把儿子的权值加到父亲上,然后儿子的权值清零。
莫队算法
-
题目大意:给定一个数列,给定查询区间[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树
-
题意:在树中找一条路径,使得这条路径的边权的异或和最大。
预处理每个节点到根的异或和是套路。然后,考虑枚举两个点,(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++; }
并查集
-
题目大意:有一些变量相等或者不等的限制条件,现在判定这些条件是否能同时满足。
相等是一种等价关系,所以我们可以考虑用并查集来表示这种二元关系。不等,意味着两个元素不应该在一个集合中,我们只需要调用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; }