CDQ分治

CDQ分治是一种在算法设计中用于优化问题解决的技术,主要应用于降维、优化动态规划和在线转离线问题。文章通过示例介绍了如何利用CDQ分治方法解决三维偏序问题和动态规划问题,例如拦截导弹问题,通过分治策略降低时间复杂度。此外,还讨论了如何将动态问题转化为静态问题,如在城市建设问题中的应用。
摘要由CSDN通过智能技术生成

CDQ

cdq指 IOI2008 金牌得主陈丹琦,%%%%%%
佩服
陈丹琦的论文

正题

CDQ分治主要有三大用武之地:
1.降维
2.优化DP
3.在线转离线

降维

对于一个三维的问题,我们可以用其降为两维
经典的例子就是三维偏序`
这里我们拿另一道题举例动态逆序对

仔细观察可以发现,若设第一个元素消失的时间t为n,并依次递减,那么一个元素有三个值(t,id,a
这就构成了三维,而对于任意点对(i,j),只要满足
t i < t j 且 i d i > i d j 且 a i < a j t_i<t_j 且id_i>id_j且a_i<a_j ti<tjidi>idjai<aj
或者 t i < t j 且 i d i < i d j 且 a i > a j t_i<t_j 且id_i<id_j且a_i>a_j ti<tjidi<idjai>aj
就可以令答案加1.
实现如下:

#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct AC{int m,v,d,id,t;}e[N<<1];
int n,m,tot;
int pos[N],a[N],c[N];ll ans[N];
bool cmp1(AC x,AC y){return x.d<y.d;}
void add(int x,int k){while (x<=n)c[x]+=k,x+=(x&(-x));}
int query(int x){int su=0;while(x)su+=c[x],x-=(x&(-x));return su;}
void cdq(int l,int r){
    if (l==r) return;
    int mid=(l+r)>>1,j=l;
    cdq(l,mid),cdq(mid+1,r);
    sort(e+l,e+mid+1,cmp1);
    sort(e+mid+1,e+r+1,cmp1);
    for (int i=mid+1;i<=r;++i){
        while (j<=mid&&e[j].d<=e[i].d)add(e[j].v,e[j].m),++j;
        ans[e[i].id]+=e[i].m*(query(n)-query(e[i].v));
    }
    for (int i=l;i<j;++i) add(e[i].v,-e[i].m);
    j=mid;
    for (int i=r;i>mid;--i){
        while (j>=l&&e[j].d>=e[i].d)add(e[j].v,e[j].m),--j;
        ans[e[i].id]+=e[i].m*query(e[i].v-1);
    }
    for (int i=mid;i>j;--i) add(e[i].v,-e[i].m);
}
int main(){
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;++i)scanf("%d",&a[i]),pos[a[i]]=i,e[++tot]=(AC){1,a[i],i,0,tot};
    for (int i=1,x;i<=m;++i)scanf("%d",&x),e[++tot]=(AC){-1,x,pos[x],i,tot};
    cdq(1,tot);
    for (int i=1;i<=m;++i) ans[i]+=ans[i-1];
    for (int i=0;i<m;++i) printf("%lld\n",ans[i]);
}

优化DP

大多数情况下,CDQ分治优化的是1D/1D 动态
1D/1D 动态规划指的是一类特定的 DP 问题,该类题目的特征是 DP 数组是一维的,转移是 O ( n ) O(n) O(n) 的。如果条件良好的话,有时可以通过 CDQ 分治来把它们的时间复杂度由 O ( n 2 ) O(n^2) O(n2)降至 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n)

例题:拦截导弹

很容易想出DP式: f i = 1 + f j ( h i < h j , v i < v j ) f_i=1+f_j(h_i<h_j , v_i<v_j) fi=1+fj(hi<hj,vi<vj)
所以得到 O ( n 2 ) O(n^2) O(n2)的做法

#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 7;
int n, f[N], ans;
struct st {
    int h, v;
} rk[N];

int main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; i++)
        scanf("%d%d", &rk[i].h, &rk[i].v);

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            if (rk[i].h <= rk[j].h && rk[i].v <= rk[j].v) {
                f[i] = max(f[i], f[j]);
            }
        }

        f[i]++;
        ans = max(ans, f[i]);
    }

    printf("%d\n", ans);

    for (int i = 1; i <= n; i++)
        printf("0.000000 ");
}

用CDQ可以优化
我们发现 f j f_{j} fj 转移到 f i f_{i} fi 这种转移关系也是一种点对间的关系,所以我们用类似 CDQ 分治处理点对关系的方式来处理它。
这个转移过程相对来讲比较套路。假设现在正在处理的区间是 (l,r),算法流程大致如下:

1.如果 l=r,说明 f r f_{r} fr 值已经被计算好了。直接令 f r f_{r} fr++ 然后返回即可;
2.递归使用 solve(l,mid);
3.处理所有 l ≤ j ≤ m i d , m i d + 1 ≤ i ≤ r l \leq j \leq mid,mid+1 \leq i \leq r ljmidmid+1ir 的转移关系;
4.递归使用 solve(mid+1,r)。

#include <bits/stdc++.h>
#define FOR(a,b,c) for(int a=b;a<=c;a++)
using namespace std;

const int N = 1e5 + 10;

struct Node {
    int id, x, y;
    bool operator<(const Node &rhs)const {
        return x < rhs.x || (x == rhs.x && y < rhs.y);
    }
} q[N], t[N];
bool cmp(const Node &a, const Node &b) {
    return a.id < b.id;
}

int f[2][N];
double g[2][N], ans[N];
int hh[N], tot, n;


int read() {
    char c = getchar();
    int x = 0;
    int ff = 1;

    while (!isdigit(c)) {
        if (c == '-')
            ff = -1;

        c = getchar();
    }

    while (isdigit(c))
        x = x * 10 + c - '0', c = getchar();

    return x * ff;
}

int t_f[N], tag[N], T;
double t_g[N];
void add(int x, int ff, double gg) {
    for (; x <= tot; x += x & -x) {
        if (tag[x] != T) {
            tag[x] = T;
            t_f[x] = 0;
            t_g[x] = 0;
        }

        if (ff > t_f[x]) {
            t_f[x] = ff;
            t_g[x] = gg;
        } else if (ff == t_f[x])
            t_g[x] += gg;
    }
}
void query(int x, int &mx, double &sum) {
    mx = 0;
    sum = 0.0;

    for (; x; x -= x & -x)
        if (tag[x] == T) {
            if (t_f[x] > mx) {
                mx = t_f[x];
                sum = t_g[x];
            } else if (t_f[x] == mx)
                sum += t_g[x];
        }
}

void solve(int l, int r, int ty) {
    if (l == r) {
        if (!f[ty][l]) {
            f[ty][l] = 1;
            g[ty][l] = 1;
        }

        return ;
    }

    int mid = (l + r) >> 1;
    int l1 = l, l2 = mid + 1, i, j, cnt = 0;

    for (i = l; i <= r; i++) {
        if (q[i].id <= mid)
            t[l1++] = q[i];
        else
            t[l2++] = q[i];
    }

    memcpy(q + l, t + l, sizeof(Node) * (r - l + 1));
    solve(l, mid, ty);
    T++;
    sort(q + mid + 1, q + r + 1);

    for (i = mid + 1, j = l; i <= r; i++) {
        int id;

        for (; j <= mid && q[j].x <= q[i].x; j++) {
            id = q[j].id;
            cnt++;
            add(q[j].y, f[ty][id], g[ty][id]);
        }

        int mx;
        double sum;
        query(q[i].y, mx, sum);
        id = q[i].id;

        if (mx > 0) {
            if (mx + 1 > f[ty][id]) {
                f[ty][id] = mx + 1;
                g[ty][id] = sum;
            } else if (mx + 1 == f[ty][id]) {
                g[ty][id] += sum;
            }
        }
    }

    solve(mid + 1, r, ty);
    l1 = l, l2 = mid + 1;
    int now = l;

    while (l1 <= mid || l2 <= r) {
        if (l2 > r || l1 <= mid && q[l1] < q[l2])
            t[now++] = q[l1++];
        else
            t[now++] = q[l2++];
    }

    memcpy(q + l, t + l, sizeof(Node) * (r - l + 1));
}

int main() {
    n = read();
    int mxx = 0;
    FOR(i, 1, n) {
        q[i].x = read(), q[i].y = read();
        hh[i] = q[i].y;
        mxx = max(mxx, q[i].x);
    }
    sort(hh + 1, hh + n + 1);
    tot = unique(hh + 1, hh + n + 1) - hh - 1;
    FOR(i, 1, n)
    q[i].y = lower_bound(hh + 1, hh + tot + 1, q[i].y) - hh;
    reverse(q + 1, q + n + 1);
    FOR(i, 1, n) q[i].id = i;
    solve(1, n, 0);

    sort(q + 1, q + n + 1, cmp);
    reverse(q + 1, q + n + 1);
    FOR(i, 1, n)
    q[i].x = mxx - q[i].x + 1, q[i].y = tot - q[i].y + 1,
        q[i].id = i;
    solve(1, n, 1);
    int mx = 0;
    double sum = 0;
    FOR(i, 1, n) {
        int tmp = f[0][i];

        if (tmp > mx) {
            mx = tmp;
            sum = g[0][i] * g[1][n - i + 1];
        } else if (tmp == mx)
            sum += g[0][i] * g[1][n - i + 1];
    }
    printf("%d\n", mx);

    for (int i = n; i; i--) {
        if (f[0][i] + f[1][n - i + 1] - 1 == mx) {
            ans[i] = (g[0][i] * g[1][n - i + 1]) / sum;
        } else
            ans[i] = 0;
    }

    for (int i = n; i > 1; i--)
        printf("%.5lf ", ans[i]);

    printf("%.5lf", ans[1]);
    return 0;
}

题外话

嘿嘿,越来越难了,对吧,没关系,还有更难的

将动态问题转化为静态问题

做法大概就是将所有操作离线,在进行询问操作时会发现,会影响其的修改操作已经全部解决了
思路简单,却极其难想
[HNOI2010] 城市建设

思路:

假设我们正在构造 ( l , r ) (l,r) (l,r) 这段区间的最小生成树边集,并且我们已知它父亲最小生成树的边集。我们将在 ( l , r ) (l,r) (l,r) 这段区间中发生变化的边分别赋与 + ∞ + \infty + − ∞ -\infty 的边权,并各跑一边 kruskal,求出在最小生成树里的那些边。
对于一条边来讲:
如果最小生成树里所有被修改的边权都被赋成了 + ∞ +\infty +,而它未出现在树中,则证明它不可能出现在 ( l , r ) (l,r) (l,r) 这些询问的最小生成树当中。所以我们仅仅在 (l,r) 的边集中加入最小生成树的树边。
如果最小生成树里所有被修改的边权都被赋成了 − ∞ -\infty ,而它出现在树中,则证明它一定会出现 ( l , r ) (l,r) (l,r) 这段的区间的最小生成树当中。这样的话我们就可以使用并查集将这些边对应的点缩起来,并且将答案加上这些边的边权。

#include<bits/stdc++.h>
#define pb push_back
#define mk make_pair
using namespace std;
typedef long long ll;
const int N=5e4+7;
int n,m,q,fa[N],sz[N],tag,ta[N];
ll ans[N];
struct edge{   
	int u,v,w;  
	bool friend operator<(edge a,edge b) { return a.w<b.w; }
}E[N];
struct Query{   int id, d;   }qu[N];
bool cmp(int x,int y){ return E[x]<E[y]; }

vector<pair<int *,int> > buc;
void push(int &x,int v){
	buc.pb(mk(&x,x));
	x=v;
}
void clear(int t){
    while(buc.size()>t) {
		*buc.back().first=buc.back().second,buc.pop_back();
	}
}
int find(int k){   return k==fa[k]?k:find(fa[k]);  }
void unite(int x,int y){
	x=find(x),y=find(y);
	if(x==y) return;
	if(sz[x]>sz[y]) swap(x,y);
	push(fa[x],y);push(sz[y],sz[x]+sz[y]);
}

void work(vector<int> &e,int l,int r,ll &val){
	int st=buc.size();
	vector<int> tmp;
	sort(e.begin(),e.end(),cmp);
	for(int i=l;i<=r;i++) unite(E[qu[i].id].u,E[qu[i].id].v);
	for(int i:e){
		if(ta[i]==tag) continue;
		int r1=find(E[i].u),r2=find(E[i].v);
		if(r1!=r2) {
			unite(r1,r2); val+=E[i].w; tmp.pb(i);
		}
	}
	clear(st);
	for(int i:tmp) unite(E[i].u,E[i].v);
}
void del(vector<int> &e){
	int st=buc.size();
	vector<int> tmp;
	sort(e.begin(),e.end(),cmp);
	for(int i:e){
		if(ta[i]==tag) {
			tmp.pb(i);
			continue;
		}
		int r1=find(E[i].u),r2=find(E[i].v);
		if(r1!=r2) {
			unite(r1,r2); tmp.pb(i);
		}
	}
	clear(st);
	e.swap(tmp);
}

void solve(int l ,int r ,vector<int> e ,ll delta ) {
	if(l==r) E[qu[l].id].w=qu[l].d;
	int st=buc.size();
	if(l==r){
		sort(e.begin(),e.end(),cmp );
		for(int i:e){
			int r1=find(E[i].u),r2=find(E[i].v);
			if(r1==r2) continue;
			unite(r1,r2);
			delta+=E[i].w;
		}
		ans[l]=delta;
	}else{
		tag++;//记录动态边
		for(int i=l;i<=r;i++) ta[qu[i].id]=tag;
		work(e,l,r,delta);//找出必用边
		del(e);//删去必不用边
		int mid=(l+r)>>1;
		solve(l,mid,e,delta); solve(mid+1,r,e,delta);
	}
	clear(st);
}

int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=n;i++) fa[i]=i,sz[i]=1;
	vector<int>ss;
	for(int i=1;i<=m;i++) {
		scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w); 
		ss.pb(i);
	}
	for(int i=1;i<=q;i++) scanf("%d%d",&qu[i].id,&qu[i].d);

	solve(1,q,ss,0);

	for(int i=1;i<=q;i++)
		printf("%lld\n",ans[i]);

	return 0;
}

题后话

路漫漫其修远兮,吾将上下而求索

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值