换汤不换药——cdq分治
cdq分治——它是一种通过计算前一半对后一半的影响的手段。
直到今天才发现其实还是挺简单的,就是把一个区间分成两部分,然后看前半部分对后半部分的贡献,然后一路分治下去就好了。
洛谷 P3810 【模板】三维偏序(陌上花开)
三维偏序问题是二维偏序问题的升级版,二维偏序通常就是第一维排个序,第二维搞个树状数组等等来求出答案。
但是三维呢?我们第一维依然是可以排序,现在只剩二维了,此时我们可以用树套树解决,动态问题的情况下这的确是种解法,但是在离线情况下,cdq分治有它的优势。
我们把第二维通过cdq分治来降维,怎么做?
我们把一个区间分成两部分,用双指针,第一个指针放在左区间,第二个指针放在右区间,不断判断左区间当前指针值是否对当前右区间指针值做出贡献,直到左区间的贡献结束。(类似归并排序求逆序对)
而是否造成贡献,取决于它们的第三维坐标。贡献(第三维)就用树状数组来处理。
#include <bits/stdc++.h> #define mp make_pair using namespace std; typedef long long ll; inline int read(){int s=0,w=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return s*w;} const int maxn = 200005; const int INF = 0x3f3f3f3f; const int mod =998244353; struct node { int a,b,c,cnt,ans; }s[maxn],x[maxn]; int n,m,k,top,ans[maxn],i; int c[maxn]; bool cmp1(node x,node y) { if (x.a==y.a) { if(x.b==y.b)return x.c<y.c; else return x.b<y.b; } else return x.a<y.a; } bool cmp2(node x,node y) { if (x.b==y.b) return x.c<y.c; else return x.b<y.b; } int lowbit(int x) { return x & (-x); } void add(int x,int y) { while (x<=k) { c[x]+=y; x+=lowbit(x); } } int query(int x) { int sum=0; while(x) { sum+=c[x]; x-=lowbit(x); } return sum; } void cdq(int l,int r) { if (l==r) return; int mid=(l+r)/2; cdq(l,mid); cdq(mid+1,r); sort(s+l,s+mid+1,cmp2); sort(s+mid+1,s+r+1,cmp2); int i,j=l; for (i=mid+1;i<=r;i++) { while(s[i].b>=s[j].b && j<=mid) { add(s[j].c,s[j].cnt); j++; } s[i].ans+=query(s[i].c); } for(i=l;i<j;++i) add(s[i].c,-s[i].cnt); } int main() { n=read();k=read(); for (i=1;i<=n;i++) { x[i].a=read(); x[i].b=read(); x[i].c=read(); } sort(x+1,x+1+n,cmp1); for(int i=1;i<=n;++i) { top++; if(x[i].a!=x[i+1].a||x[i].b!=x[i+1].b||x[i].c!=x[i+1].c) { m++; s[m].a=x[i].a; s[m].b=x[i].b; s[m].c=x[i].c; s[m].cnt=top; top=0; } } cdq(1,m); for(int i=1;i<=m;++i) ans[s[i].ans+s[i].cnt-1]+=s[i].cnt; for(int i=0;i<n;++i) printf("%d\n",ans[i]); return 0; }
牛客https://ac.nowcoder.com/acm/contest/625/H
求ans
对于当前区间【l,r】而言,我们把他分治为【l,mid】、【mid+1、r】,于是我们根据cdq分治的原理前半部分对后半部分的贡献,我们算出左区间的一段gcd值,然后与右区间的每一段各自的gcd值,进行处理。这里我们用两个map来存取左右区间的gcd值,即可达到目的。这里要注意的是前半部分如何对后半部分产生贡献呢?我们可以看到左区间的gcd若要与右区间产生联系,那么肯定是从后往前的。比如,你不可能【l,l+1】这段区间的gcd值能与【mid+1,r】的gcd值关联,因为我们要保持连续性,所以只能【mid-x,mid】与【mid+1,r】联系起来,即要从后往前算gcd值。
#include <bits/stdc++.h> #define mp make_pair using namespace std; typedef long long ll; inline int read(){int s=0,w=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return s*w;} const int maxn = 5e5+5; const int INF = 0x3f3f3f3f; const int mod =1e9+7; map<int,int> G1,G2; int n,a[maxn]; ll ans; ll gcd(ll a, ll b) { return b ? gcd(b,a%b) : a; } void solve(int l,int r) { if (l==r) { ans=(ans+a[l])%mod; return; } int mid=(l+r)/2; solve(l,mid); solve(mid+1,r); int k=0; for (int i=mid;i>=l;i--) { k=gcd(k,a[i]); G1[k]++; } k=0; for (int i=mid+1;i<=r;i++) { k=gcd(k,a[i]); G2[k]++; } for (auto i:G1) for (auto j:G2) { ll num=gcd(i.first,j.first)*i.second%mod*j.second%mod; ans=(ans+num)%mod; } G1.clear(); G2.clear(); } int main() { n=read(); for (int i=1;i<=n;i++) a[i]=read(); solve(1,n); printf("%lld\n",ans); return 0; }
说实话,在没有了解这个算法之前,我一直以为它十分的高深,因为前面有个cdq的名称。我以前的疑问:cdq是什么?人名……。普通分治上面还能怎么优化?其实只是在分治的前提下计算前半部分对后半部分的贡献……。以前看别人题解写“这题cdq分治水过啦”,???黑人问号,这算法听起来超级高深的亚子。
总结:其实阻碍我们在acm道路上成长,不是这些算法有多么的高深,而是这些算法很多且杂,题目的种类也是繁多,萌新往往会被一个未成学过的算法给吓唬到,以至于解题无所适从。所以坚持就显得尤为重要,因为,你哪知道前方的风景会有多美。