并查集:
作用:1.动态维护若干个点之间的关系,若有多个关系可以用边带权,不同的权值代表不同的关系。2.一个并查集就是一个连通块。
题意:给定一个01序列S。现在有M个描述,每个描述如下 L 到 R当中有奇数个1 。或者 L 到 R中有偶数个1.现在发现其中有些描述是矛盾的,比如 [ 1 , 3 ]有奇数个1.[ 4,6 ]有偶数个1,现在又说[ 1,6 ]
有偶数个1.所以问至少多少个描述后可以检测到矛盾出现。
思路: S[ l~r ] 里有偶数个1,等价于 sum[ l-1 ]和 sum[ r ]奇偶性相同.
S[ l~r ]有奇数个1 等价于sum[ l-1 ]和sum[ r ]奇偶性不同。
区间里1的个数,可以转化为两个前缀和奇偶性的关系。
则现在这道题和程序自动分析就比较像了,程序自动分析描述了两个点之间的先等或不等的关系。这道题描述了两个变量奇偶性同或不同的关系。
所以这道题也是并查集动态维护关系。两者之间的区别,本题维护了两个关系,而程序分析只维护了一个关系。
技巧:1. 由于维护的只有两个关系,所以可以给边权赋值0或1.出现0,1想到异或。并且边带权的并查集维护的 d 数组都是某个点到根节点的距离。
所以这道题也可以用一个 d 数组维护每个点一路异或到根节点的异或和来表示它和根节点的关系。要判断每两个节点之间的关系。通过它们各自和根节点的关系
来判断出它们的关系。
2.奇偶性和异或和很多性质是相通的。
3.异或和的性质: 1. a1 xor a2 xor a3 xor a4.....若要计算 a2 xor a3 xor a4 ==sum[2-1] xor sum[4]. 2.ans=a xor b xor c 则c=ans xor a xor b 。
4.给定一堆数字,要算出某个数和这里面哪个数的异或和最大。则这堆数字建立字典树。然后再用这个数字去异或
5.这道题给定的区间范围很大,l r 最大能到10 e9级别。但是询问却很少,所以可以把 l r离散到 询问的数量范围内。
#include<iostream> #include<cstdio> #include<map> #include<string> using namespace std; const int maxn=1e5+10; int N,M,tot,k; int fa[2*maxn]; int d[2*maxn]; struct node{ int l,r,ans; }query[2*maxn]; map<int,int> mx; string str; int discreat(int x) { if(mx.count(x)==1) return mx[x]; else return mx[x]=++tot; } int get(int x) { if(x==fa[x]) return x; int root=get(fa[x]);d[x]^=d[fa[x]]; return fa[x]=root; } int main() { cin>>N; cin>>M; int x,y; for(int i=1;i<=M;i++) { cin>>x>>y>>str; if(str[0]=='o') query[i].ans=1; else query[i].ans=0; query[i].l=x-1; query[i].r=y; } for(int i=1;i<=2*maxn;i++) fa[i]=i; for(int i=1;i<=M;i++) { x=discreat(query[i].l); y=discreat(query[i].r); int p=get(x),q=get(y); if(p==q){ //cout<<"111111111\n"; if((d[x]^d[y])!=query[i].ans) { cout<<i-1<<"\n";return 0; } } else { fa[p]=q,d[p]=d[x]^d[y]^query[i].ans; } } cout<<M<<"\n"; return 0; } /* 10 5 1 2 even 3 4 odd 5 6 even 1 6 even 7 10 odd */
食物链:算法进阶指南197;
技巧:设置f[ i ] 表示i号动物的同类域 , f[ i+n ]表示了 i 号动物的捕食域, f[ i+n+n ]为当前动物的天敌域。则接下来只要根据每句话来给每个动物的三个域 添加相应的动物就可以
注意:为什么判断当前话语是否矛盾时,不能直接判断当前两个动物是否同类,因为,当前动物还没有添加进自己的同类域里。
get(x)==get(y+n)||get(x)==get(y+n+n)
x==y||get(x)==get(y+n)||get(x)==get(y)
#include<bits/stdc++.h> using namespace std; const int maxn=5e4+10; int n,k,m,ans=0; int fa[3*maxn]; int get(int x) { if(x==fa[x]) return x; return fa[x]=get(fa[x]); } void merge_(int x,int y) { fa[get(x)]=get(y); } int main() { scanf("%d%d",&n,&k); for(int i=1;i<=3*n;i++) fa[i]=i; for(int i=1;i<=k;i++) { int x,y; scanf("%d%d%d",&m,&x,&y); if(x>n||y>n) ans++; else if(m==1) { if(get(x)==get(y+n)||get(x)==get(y+n+n)) ans++; else { merge_(x,y); merge_(x+n,y+n); merge_(x+n+n,y+n+n); } } else if(m==2) { if(x==y||get(x)==get(y+n)||get(x)==get(y)) ans++; else { merge_(x,y+n+n); merge_(x+n+n,y+n); merge_(x+n,y); } } } cout<<ans; return 0; }
树状数组:利用了二进制倍增的思想, c[ x ]表示了x 写成二进制的情况下,从右往左第一个为 1 的二进制位代表的长度范围内的数的前缀和。
所以如果不停的用x-=lowbit(x) 就可以把整个x长度范围内的数全部相加起来,这就是前缀和查询操作。
int ask(int x)
{
int ans=0;
for(;x;x-=x&-x) ans+=c[x];
return ans;
}
添加操作:如果是减去某个数只要传进去一个负数就可以了
void add(int x,int y)
{
for(;x<=n;x+=x&-x) c[x]+=y;
}
题意:用树状数组求逆序对。(归并排序也可以求逆序对)
思路:把序列倒着插进去树状数组就可以了,然后统计一遍每个数的前缀和,这样就是小于它并且下标还大于它的数的数量。
for( int i=n; i ; i-- )
{
ans+=ask( a[ i ] - 1 );
add((a[ i ], 1 );
}
楼兰图腾(算法进阶指南201):求V字和倒V字型的数量。
思路:用树状数组求逆序对。
using namespace std; const int maxn=2e5+10; const int inf=0x3f3f3f3f; typedef unsigned long long ull; int n; int y[maxn]; int c[maxn]; int right0[maxn]; int left0[maxn]; int cnt=0; ull sum1,sum2; int ask(int x) { int ans=0; for(;x;x-=x&-x) ans+=c[x]; return ans; } void add(int x,int y) { for(;x<=n;x+=x&-x) c[x]+=y; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&y[i]); for(int i=n;i>=1;i--) { int ans=ask(y[i]-1); add(y[i],1); right0[i]=ans; } memset(c,0,sizeof c); for(int i=1;i<=n;i++) { int ans=ask(y[i]-1); add(y[i],i); left0[i]=ans; } sum2=0; for(int i=2;i<n;i++) sum2+=(ull)(left0[i]*right0[i]); memset(right0,0,sizeof right0); memset(left0,0,sizeof left0); memset(c,0,sizeof c); for(int i=n;i>=1;i--) { cnt++; int ans=ask(y[i]-1); right0[i]=cnt-ans-1; add(y[i],1); } memset(c,0,sizeof c); for(int i=1;i<=n;i++) { int ans=ask(y[i]-1); left0[i]=i-ans-1; add(y[i],1); } sum1=0; for(int i=2;i<n;i++) { sum1+=(ull)(left0[i]*right0[i]); } cout<<sum1<<" "<<sum2; return 0; }
树状数组拓展:
把单点修改变为给某个左右区间里的数字都加上 d 。树状数组维护一个 b 数组,若 l~r 加 d 。则 b[ l ]+=d, b[ r+1 ]-=d.最后询问某个数字的值时只需要询问b数组的前缀 和,就可以知道修改操作对当前数字的影响。
题意(算法进阶指南205):有n头奶牛,身高1~n,现在n头奶牛站成一排,已知 i 头奶牛前面有Ai头奶牛比它高。问你每头奶牛的身高。
思路:每头奶牛的身高受两个因素影响,一是它前面比它高的奶牛数,二是后面比它高的奶牛数。这种双因素影响问题的解的问题,考虑去除一个因素的影响。
则可以从后往前考虑,先考虑最后一头奶牛,最后一头奶牛的身高就确立了。
具体做法:树状数组维护一个b数组,起初b数组全部为 1 .然后对于每一个人二分他的身高mid,然后去树状数组里判断是否有c[ mid ]是否满断是否符合
技巧:这种考虑相互之间影响的情况,好像特别喜欢用一个b数组通过加减来维护
#include<bits/stdc++.h> using namespace std; const int maxn=1e5+10; int N; int a[maxn]; int c[maxn]; int h[maxn]; int ask(int x) { int ans=0; for(;x;x-=x&-x) ans+=c[x]; return ans; } void add(int x,int y) { for(;x<=N;x+=x&-x) c[x]+=y; } int main() { scanf("%d",&N); for(int i=2;i<=N;i++) scanf("%d",&a[i]); add(1,1); for(int i=2;i<=N;i++) { add(i,1); } for(int i=N;i>=1;i--) { int l=1,r=N; while(l<r){ int mid=(l+r)>>1; if(ask(mid)<a[i]+1) l=mid+1;else r=mid; } h[i]=r; add(r,-1); } for(int i=1;i<=N;i++) cout<<h[i]<<"\n"; return 0; }
线段树:
基于分治的思想,每个大区间的信息由它的两个子区间的信息得出。
Interval GCD.
长度为N的数列A,有M条指令,1 把区间 l~r里的数字全部加上d. 2.询问A[ l ],A[ l+1 ], A[ l+2 ],.....A[ r ]的最大公约数。
技巧:gcd(A[ l ],A[ l+1 ], A[ l+2 ],.....A[ r ] )=gcd(A[l] ,A[L+1]-A[L],A[L+2]-A[L+1],......A[R]-A[R-1]).。一般可构造差分数列
思路:线段树维护差分序列B的最大公约数,树状数组单点维护A序列的值。
#include<bits/stdc++.h> using namespace std; const int maxn=5e5+10; typedef long long ll; struct SegmentTree1{ ll l,r; ll dat; }t[4*maxn]; ll gcd(ll a,ll b) { return b?gcd(b,a%b):a; } ll N,M; ll A[maxn]; ll B[maxn]; ll c[maxn]; void build(ll p,ll l,ll r) { t[p].l=l,t[p].r=r; if(l==r){t[p].dat=B[l];return;} ll mid=(l+r)>>1; build(p*2,l,mid); build(p*2+1,mid+1,r); t[p].dat=gcd(t[p*2].dat,t[p*2+1].dat); } void change(ll p,ll x,ll v) { if(t[p].l==t[p].r){t[p].dat+=v;return;} ll mid=(t[p].l+t[p].r)>>1; if(x<=mid) change(p*2,x,v); else change(p*2+1,x,v); t[p].dat=gcd(t[p*2].dat,t[p*2+1].dat); } ll ask(ll p,ll l,ll r){ if(l<=t[p].l&&r>=t[p].r) return t[p].dat; ll mid=(t[p].l+t[p].r)>>1; ll val=0; if(l<=mid) val=gcd(val,ask(p*2,l,r)); if(r>mid) val=gcd(val,ask(p*2+1,l,r)); return (ll)abs(val); } ll ask2(ll x) { ll ans=0; for(;x;x-=x&-x) ans+=c[x]; return ans; } void add(ll x,ll y) { for(;x<=N;x+=x&-x) c[x]+=y; } int main() { scanf("%d%d",&N,&M); for(int i=1;i<=N;i++) scanf("%lld",&A[i]); for(int i=1;i<=N;i++) B[i]=A[i]-A[i-1]; build(1,1,N); char op; ll x,y,d; for(int i=1;i<=M;i++) { cin>>op>>x>>y; if(op=='C') { cin>>d; add(x,d); add(y+1,-d); change(1,x,d); if(y<N) change(1,y+1,-d); } else if(op=='Q') { ll ans_1=A[x]+ask2(x); ll ans_2=ask(1,x+1,y); cout<<gcd(ans_1,ans_2)<<"\n"; } } return 0; }
分块:大块维护,小块朴素。首先你要知道你要维护的信息是什么,然后每次对l~r区间进行修改时,把 l~r 当中存在的完整区间,只需要修改其维护的信息就可以了。而对于不足一个区间的因为
它的量很小,只能一个个的修改,但是又因为范围小,所以复杂度可以接受。而询问操作的时候,l~r区间中的整个区间的对答案的贡献也可以直接由维护的信息值推出
磁力块(算法进阶指南222):
技巧:每个磁力块能否被吸引过来受到两个因素的影响,一是它的质量。二是它的距离。所以我们应当考虑如何排除一个因素的影响。
思路:先按照质量排个序,然后分成 ‘根号N’ 块,每块的大小是 ‘根号N’。每块内部再按照距离排序。
为防止一个磁力块重复入队,每块的内部再一些磁力块入队后把指针打到入队的块的右边的石块位置。
#include<bits/stdc++.h> using namespace std; const int maxn=260000; struct node { int x,y; int m,p,r; double dis; }stone[260000]; int x0,y_0,p0,r0,n,t,ans; int L[maxn],R[maxn],cur[maxn],weight[maxn]; queue<node> Q; bool cmp(node a,node b) { return a.m<b.m; } bool cmp2(node a,node b) { return a.dis<b.dis; } int main() { cin>>x0>>y_0>>p0>>r0>>n; stone[0].x=x0,stone[0].y=y_0,stone[0].p=p0,stone[0].r=r0; for(int i=1;i<=n;i++) { cin>>stone[i].x>>stone[i].y>>stone[i].m>>stone[i].p>>stone[i].r; stone[i].dis=(x0-stone[i].x)*(x0-stone[i].x)+(y_0-stone[i].y)*(y_0-stone[i].y); } sort(stone+1,stone+1+n,cmp); t=sqrt(n); for(int i=1;i<=t;i++) { L[i]=(i-1)*sqrt(n)+1; R[i]=i*sqrt(n); weight[i]=stone[R[i]].m; cur[i]=L[i]; } if(R[t]<n) {++t;L[t]=R[t-1]+1;R[t]=n;weight[t]=stone[R[t]].m;cur[t]=L[t];} cout<<"+_+_+_+_+_+_++_+_+_+_+\n"; for(int i=1;i<=n;i++) cout<<stone[i].m<<" "<<stone[i].dis<<"\n"; cout<<"+_+_+_+_+_+_+_+_+_+_+_+_+_+_+\n"; for(int i=1;i<=t;i++) { cout<<L[i]<<" "<<R[i]<<" "<<weight[i]<<"\n"; } cout<<"+++++++++++++++++++++++++++++++\n"; for(int i=1;i<=t;i++) { sort(stone+L[i],stone+R[i]+1,cmp2); } for(int i=1;i<=t;i++) { for(int j=L[i];j<=R[i];j++) cout<<stone[j].dis<<" "; cout<<"\n"; } cout<<"-----------------------------------------\n"; Q.push(stone[0]); while(!Q.empty()) { node base; base=Q.front(); Q.pop(); cout<<"base.p: "<<base.p<<" base.r: "<<base.r<<"\n"; ans++; int k; for(int i=1;i<=t;i++) { if(weight[i]<=base.p) { for(int j=cur[i];j<=R[i];j++) { if(stone[j].dis<=base.r*base.r) { cout<<"Èë¶ÓÔªËØ: "<<stone[j].m<<"\n"; Q.push(stone[i]); } else {cur[i]=j;break;} } } else{ k=i;cout<<"k: "<<k<<"\n";break; } } for(int j=cur[k];j<=R[k];j++) { if(stone[j].dis<=base.r*base.r&&stone[j].m<=base.p) { Q.push(stone[j]); } else { cur[k]=j; break; } } } cout<<ans; return 0; }
点分治的内容还没完全搞懂,等做完了题回来补充