分治算法
基础算法:分治算法完全详解(含多项式应用)
第一部分:分治算法基本思想
分治(Divide and Conquer)将原问题分解为若干规模较小但结构相同的子问题,递归求解,再合并子问题的解。三步流程:
- 分解:将问题划分成若干独立子问题。
- 解决:递归求解子问题(规模足够小时直接求解)。
- 合并:将子问题的解合并为原问题的解。
适用条件:
- 子问题可独立求解(无重叠,若有重叠则用动态规划)。
- 子问题解可合并。
复杂度:通常由递推式 \(T(n)=aT(n/b)+f(n)\) 由主定理给出。
第二部分:经典分治算法实现
1. 归并排序(Merge Sort)
时间复杂度 \(O(n\log n)\),空间 \(O(n)\)(辅助数组)。
void merge(vector<int>& a, int l, int mid, int r) {
vector<int> temp(r-l+1);
int i=l, j=mid+1, k=0;
while(i<=mid && j<=r) {
if(a[i]<=a[j]) temp[k++]=a[i++];
else temp[k++]=a[j++];
}
while(i<=mid) temp[k++]=a[i++];
while(j<=r) temp[k++]=a[j++];
for(int p=0;p<k;p++) a[l+p]=temp[p];
}
void mergeSort(vector<int>& a, int l, int r) {
if(l>=r) return;
int mid=(l+r)/2;
mergeSort(a,l,mid);
mergeSort(a,mid+1,r);
merge(a,l,mid,r);
}
2. 快速排序(Quick Sort)
平均 \(O(n\log n)\),最坏 \(O(n^2)\)(随机化基准避免退化),空间 \(O(\log n)\)(递归栈)。
int partition(vector<int>& a, int l, int r) {
int pivot=a[r];
int i=l-1;
for(int j=l;j<r;j++) {
if(a[j]<=pivot) { i++; swap(a[i],a[j]); }
}
swap(a[i+1],a[r]);
return i+1;
}
int randomPartition(vector<int>& a, int l, int r) {
int randIdx=l+rand()%(r-l+1);
swap(a[randIdx],a[r]);
return partition(a,l,r);
}
void quickSort(vector<int>& a, int l, int r) {
if(l>=r) return;
int p=randomPartition(a,l,r);
quickSort(a,l,p-1);
quickSort(a,p+1,r);
}
3. 最大子段和(最大子数组)
分治求跨中点最大和,时间复杂度 \(O(n\log n)\)。
int maxCrossingSum(const vector<int>& a, int l, int mid, int r) {
int leftSum=INT_MIN, rightSum=INT_MIN, sum=0;
for(int i=mid;i>=l;i--) { sum+=a[i]; leftSum=max(leftSum,sum); }
sum=0;
for(int i=mid+1;i<=r;i++) { sum+=a[i]; rightSum=max(rightSum,sum); }
return leftSum+rightSum;
}
int maxSubArray(const vector<int>& a, int l, int r) {
if(l==r) return a[l];
int mid=(l+r)/2;
int left=maxSubArray(a,l,mid);
int right=maxSubArray(a,mid+1,r);
int cross=maxCrossingSum(a,l,mid,r);
return max({left,right,cross});
}
4. 二分搜索(递归实现)
时间复杂度 \(O(\log n)\),空间 \(O(\log n)\)(递归栈)。
int binarySearchRec(const vector<int>& a, int l, int r, int target) {
if(l>r) return -1;
int mid=l+(r-l)/2;
if(a[mid]==target) return mid;
else if(a[mid]<target) return binarySearchRec(a,mid+1,r,target);
else return binarySearchRec(a,l,mid-1,target);
}
5. 快速幂(Fast Exponentiation)
递归实现,时间复杂度 \(O(\log n)\),空间 \(O(\log n)\)(递归栈)。
int fastPow(int a, int n) {
if(n==0) return 1;
int half=fastPow(a,n/2);
if(n%2==0) return half*half;
else return half*half*a;
}
6. 最近点对问题(Closest Pair of Points)
分治按 x 排序,递归求左右最近距离,再检查跨中线带状区域。时间复杂度 \(O(n\log n)\)。
struct Point { int x,y; };
bool cmpX(const Point& a,const Point& b){ return a.x<b.x; }
bool cmpY(const Point& a,const Point& b){ return a.y<b.y; }
double dist(const Point& a,const Point& b){ return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); }
double closestUtil(vector<Point>& pts, int l, int r) {
if(r-l<=3) { double d=1e18; for(int i=l;i<r;i++) for(int j=i+1;j<=r;j++) d=min(d,dist(pts[i],pts[j])); return d; }
int mid=(l+r)/2;
int midX=pts[mid].x;
double dl=closestUtil(pts,l,mid);
double dr=closestUtil(pts,mid+1,r);
double d=min(dl,dr);
vector<Point> strip;
for(int i=l;i<=r;i++) if(abs(pts[i].x-midX)<d) strip.push_back(pts[i]);
sort(strip.be,strip.en,cmpY);
for(int i=0;i<strip.size();i++) for(int j=i+1;j<strip.size() && (strip[j].y-strip[i].y)<d;j++) d=min(d,dist(strip[i],strip[j]));
return d;
}
double closestPair(vector<Point>& pts){ sort(pts.be,pts.en,cmpX); return closestUtil(pts,0,pts.size()-1); }
第三部分:分治算法的变形与进阶
1. CDQ 分治
用于离线处理动态问题(如偏序、动态规划),按时间分治,处理左半对右半的贡献。典型应用:三维偏序计数(归并排序+树状数组)。
struct Node { int a,b,c,id; };
vector<int> ans;
void cdq(int l,int r, vector<Node>& nodes) {
if(l==r) return;
int mid=(l+r)/2;
cdq(l,mid,nodes);
cdq(mid+1,r,nodes);
vector<Node> tmp;
int i=l, j=mid+1;
BIT bit(maxC);
while(i<=mid && j<=r) {
if(nodes[i].b <= nodes[j].b) { bit.add(nodes[i].c,1); tmp.push_back(nodes[i++]); }
else { ans[nodes[j].id] += bit.sum(nodes[j].c); tmp.push_back(nodes[j++]); }
}
while(i<=mid) tmp.push_back(nodes[i++]);
while(j<=r) { ans[nodes[j].id] += bit.sum(nodes[j].c); tmp.push_back(nodes[j++]); }
for(int k=l;k<=mid;k++) bit.add(nodes[k].c,-1);
for(int k=l;k<=r;k++) nodes[k]=tmp[k-l];
}
2. 整体二分(Parallel Binary Search)
对多个询问同时二分答案,利用分治将所有询问一起处理,常用于求解第 k 大、动态区间第 k 小。
void solve(int l,int r, vector<Query> queries) {
if(queries.empty()) return;
if(l==r) { for(q:queries) ans[q.id]=l; return; }
int mid=(l+r)/2;
// 将值在 [l,mid] 的修改加入 BIT
// 划分 queries 到 left 和 right
solve(l,mid,left); solve(mid+1,r,right);
}
3. 点分治(树分治)
用于统计树上路径问题(如距离为 k 的路径数)。每次选重心,处理经过重心的路径,再递归子树。复杂度 \(O(n\log n)\)。
vector<vector<pair<int,int>>> adj;
vector<bool> removed;
int sz[N], maxSub[N];
int getSize(int u,int fa) { sz[u]=1; for(auto [v,w]:adj[u]) if(v!=fa && !removed[v]) sz[u]+=getSize(v,u); return sz[u]; }
int getCentroid(int u,int fa,int total) {
maxSub[u]=total-sz[u];
for(auto [v,w]:adj[u]) if(v!=fa && !removed[v]) { maxSub[u]=max(maxSub[u],sz[v]); int c=getCentroid(v,u,total); if(c!=-1) return c; }
if(maxSub[u]<=total/2) return u;
return -1;
}
void dfsDist(int u,int fa,int dist, vector<int>& dists) {
dists.push_back(dist);
for(auto [v,w]:adj[u]) if(v!=fa && !removed[v]) dfsDist(v,u,dist+w,dists);
}
int calcPath(int u,int initDist) {
vector<int> dists; dfsDist(u,0,initDist,dists);
sort(dists.be,dists.en);
int res=0;
int i=0,j=dists.size()-1;
while(i<j) {
if(dists[i]+dists[j]==k) { /* 计数 */ }
else if(dists[i]+dists[j]<k) i++;
else j--;
}
return res;
}
void divide(int u) {
getSize(u,0); int total=sz[u];
int cen=getCentroid(u,0,total);
removed[cen]=true;
ans += calcPath(cen,0);
for(auto [v,w]:adj[cen]) if(!removed[v]) ans -= calcPath(v,w);
for(auto [v,w]:adj[cen]) if(!removed[v]) divide(v);
}
第四部分:分治与多项式运算
分治思想在多项式运算中表现为倍增法(牛顿迭代)和CDQ分治(卷积递推)。以下给出完整实现,基于 NTT 加速。
NTT 模板(用于多项式卷积)
const int MOD=998244353, G=3;
int modpow(int a,int e){ int r=1; while(e){ if(e&1) r=r*a%MOD; a=a*a%MOD; e>>=1;} return r; }
void ntt(vector<int>& a,bool invert){
int n=a.size();
for(int i=1,j=0;i<n;i++){
int bit=n>>1;
for(; j&bit; bit>>=1) j^=bit;
j^=bit;
if(i<j) swap(a[i],a[j]);
}
for(int len=2; len<=n; len<<=1){
int wlen=modpow(G, (MOD-1)/len);
if(invert) wlen=modpow(wlen, MOD-2);
for(int i=0;i<n;i+=len){
int w=1;
for(int j=0;j<len/2;j++){
int u=a[i+j], v=a[i+j+len/2]*w%MOD;
a[i+j]=(u+v)%MOD;
a[i+j+len/2]=(u-v+MOD)%MOD;
w=w*wlen%MOD;
}
}
}
if(invert){
int inv_n=modpow(n, MOD-2);
for(int &x:a) x=x*inv_n%MOD;
}
}
vector<int> convolution(const vector<int>& a,const vector<int>& b){
if(a.empty()||b.empty()) return {};
int n=a.size(), m=b.size(), sz=1;
while(sz<n+m-1) sz<<=1;
vector<int> fa(a.be,a.en), fb(b.be,b.en);
fa.resize(sz); fb.resize(sz);
ntt(fa,false); ntt(fb,false);
for(int i=0;i<sz;i++) fa[i]=fa[i]*fb[i]%MOD;
ntt(fa,true);
fa.resize(n+m-1);
return fa;
}
1. 多项式求逆(Polynomial Inversion)
给定 \(A(x)\),求 \(B(x)\) 使 \(A B \equiv 1 \pmod{x^n}\)。倍增法,每轮迭代 \(B \leftarrow B(2 - A B)\)。
vector<int> polyInv(const vector<int>& A,int n){
vector<int> B(1, modpow(A[0], MOD-2));
int m=1;
while(m<n){
int len=m*2;
vector<int> f(A.begin(), A.begin()+min((int)A.size(), len));
f.resize(len);
vector<int> g=B;
g.resize(len);
ntt(f,false); ntt(g,false);
for(int i=0;i<len;i++) f[i]=f[i]*g[i]%MOD;
ntt(f,true);
f.resize(len);
f[0]=(2-f[0]+MOD)%MOD;
for(int i=1;i<len;i++) f[i]=(MOD-f[i])%MOD;
vector<int> B2=convolution(B, f);
B2.resize(len);
B=B2;
m=len;
}
B.resize(n);
return B;
}
2. 多项式开根(Polynomial Square Root)
问题:求 \(B(x)\) 使得 \(B(x)^2 \equiv A(x) \pmod{x^n}\),要求常数项 \(A[0]\) 为二次剩余(通常为 1)。
算法:同样使用牛顿迭代。
- 已知 \(B_0\) 满足模 \(x^m\) 的开根,则模 \(x^{2m}\):\[B(x) \equiv \frac{1}{2}\left(B_0(x) + A(x)\cdot B_0^{-1}(x)\right) \pmod{x^{2m}} \]
- 每轮需要求 \(B_0\) 的逆,可用之前求逆函数(倍增时复用)。
vector<int> polySqrt(const vector<int>& A,int n){
vector<int> B(1, 1);
int m=1;
while(m<n){
int len=m*2;
vector<int> f(A.begin(), A.begin()+min((int)A.size(), len));
f.resize(len);
vector<int> invB=polyInv(B, len);
vector<int> h=convolution(B, B);
h.resize(len);
for(int i=0;i<len;i++) f[i]=(f[i]-h[i]+MOD)%MOD;
vector<int> delta=convolution(f, invB);
delta.resize(len);
int inv2=(MOD+1)/2;
for(int i=0;i<len;i++) delta[i]=delta[i]*inv2%MOD;
for(int i=0;i<len;i++) B[i]=(B[i]+delta[i])%MOD;
B.resize(len);
m=len;
}
B.resize(n);
return B;
}
3. 多项式除法与取模(Polynomial Division)
利用翻转和求逆,求商和余数。
vector<int> polyRev(const vector<int>& a){ vector<int> b=a; reverse(b.be,b.en); return b; }
vector<int> polyDiv(const vector<int>& A,const vector<int>& B){
int n=A.size()-1, m=B.size()-1;
if(n<m) return {0};
vector<int> Ar=polyRev(A), Br=polyRev(B);
Br.resize(n-m+1);
vector<int> invBr=polyInv(Br, n-m+1);
vector<int> Qr=convolution(Ar, invBr);
Qr.resize(n-m+1);
reverse(Qr.be,Qr.en);
return Qr;
}
vector<int> polyMod(const vector<int>& A,const vector<int>& B){
vector<int> Q=polyDiv(A,B);
vector<int> R=convolution(Q,B);
R.resize(A.size());
for(int i=0;i<A.size();i++) R[i]=(A[i]-R[i]+MOD)%MOD;
while(!R.empty() && R.back()==0) R.pop_back();
return R;
}
4. 分治 FFT(CDQ 分治优化卷积递推)
求解 \(dp[i] = \sum_{j=1}^{i} dp[i-j] \cdot f[j]\),\(dp[0]=1\)。分治左半贡献到右半。
vector<int> dp, f;
void cdqFFT(int l,int r){
if(l==r){ if(l==0) dp[l]=1; return; }
int mid=(l+r)/2;
cdqFFT(l,mid);
int len1=mid-l+1, len2=r-l+1;
vector<int> A(len1), B(len2);
for(int i=l;i<=mid;i++) A[i-l]=dp[i];
for(int i=1;i<=r-l;i++) B[i]=f[i];
vector<int> C=convolution(A,B);
for(int i=mid+1;i<=r;i++) dp[i]=(dp[i]+C[i-l])%MOD;
cdqFFT(mid+1,r);
}
// 调用:dp.assign(n,0); cdqFFT(0,n-1);
第五部分:小结
分治算法是算法设计的核心范式,其变种(CDQ、整体二分、点分治)可解决大量复杂问题;而在多项式领域,倍增法与分治FFT更是现代算法库的基石。
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23043067

浙公网安备 33010602011771号