• CDQ分治 & 整体分治


    Part 1:CDQ分治

    CDQ分治讲解博客

    可以把CDQ分治理解为类似与归并排序求逆序对个数的一种分治算法(至少我现在是这么想的)。先处理完左右两边各自对答案的贡献,在处理跨越左右两边的对答案的贡献。

    例题:

    逆序对(二维偏序)

    过水,不讲。

    三维偏序

    第一维先sort,第二维由归并保证,第三维在归并时查询权值树状数组。

    (Code:)

    int n, k, tot;
    struct node{
    	int a, b, c, w, id;
    }p[N], tp[N];
    int ans[N];
    ll tre[NN];
    inline void add(int cur, int ad) {
    	for (register int i = cur; i <= k; i += lowbit(i)) {
    		tre[i] += ad;
    	}
    }
    inline ll query(int cur) {
    	ll res = 0;
    	for (register int i = cur; i; i -= lowbit(i)) {
    		res += tre[i];
    	}
    	return res;
    }
    bool cmp(const node &a, const node &b) {
    	if (a.a == b.a) {
    		if (a.b == b.b) {
    			return a.c < b.c;
    		} else {
    			return a.b < b.b;
    		}
    	}
    	return a.a < b.a;
    }
    inline bool comp(const node &a, const node &b) {
    	return a.a == b.a && a.b == b.b && a.c == b.c;
    }
    void cdq(int l, int r) {
    	if (l == r) {
    		ans[p[l].id] += p[l].w;
    		return ;
    	}
    	int mid = (l + r) >> 1;
    	cdq(l, mid); cdq(mid + 1, r);
    	int i = l, j = mid + 1, top = l - 1;
    	while (i <= mid && j <= r) {
    		if (p[i].b <= p[j].b) {
    			tp[++top] = p[i];
    			add(p[i].c, p[i].w);
    			++i;
    		} else {
    			ans[p[j].id] += query(p[j].c);
    			tp[++top] = p[j];
    			++j;
    		}
    	}
    	while (i <= mid) {
    		tp[++top] = p[i];
    		add(p[i].c, p[i].w);
    		++i;
    	}
    	while (j <= r) {
    		ans[p[j].id] += query(p[j].c);
    		tp[++top] = p[j];
    		++j;
    	}
    	for (register int i = l; i <= mid; ++i) {
    		add(p[i].c, -p[i].w);
    	}
    	for (register int i = l; i <= r; ++i) {
    		p[i] = tp[i];
    	}
    }
    ll bin[N];
    int main() {
    	read(n); read(k);
    	int aa, bb, cc;
    	for (register int i = 1; i <= n; ++i) {
    		read(aa); read(bb); read(cc);
    		p[i + 1] = (node){aa, bb, cc, 1, i};
    	}
    	sort(p + 2, p + 2 + n, cmp);
    	for (register int i = 1; i <= n; ++i) {//去重
    		if (!comp(p[i], p[i + 1])) {
    			p[++tot] = p[i + 1];
    		} else {
    			p[tot].w++;
    		}
    	}
    	cdq(1, tot);
        ...
    }
    

    P3120 [USACO15FEB]Cow Hopscotch G

    题意

    现有递推式:

    [f[i][j] = sum_{u>i,v>j,id[u][v] ot = id[i][j]}{f[u][v]} ]

    [f[n][m] = 1 ]

    其中 (n, m<=750,id[i][j] <= n * m),求 (f[1][1])

    题解

    一看是三维偏序,我们就应该能想到CDQ分治

    (id[u][v] ot= id[i][j]) 可以拆成严格大于和严格小于,然后就是俩三维偏序问题了。把对行数的分治套在外面,列直接暴力。我们从右往左枚举每一列,这样枚举到上面的时候,其右下部分都已经被统计。 实际上,我们还有更好的方法,不用拆 ( ot =),放弃树状数组,能砍掉一个 (log)。具体方法是:我们可以直接维护当前 (f) 的总和 (Tot),以及其中每一个颜色编号的总和 (sum[id])。由于 (id) 比较稀疏,我们无法每次都清空 (sum),因此我们记一个 (T),表示记录信息的时间,如果发现时间与当前不符的话,就清空重记。

    然而,本题还和普通CDQ分治不同,我们原先并不知道每个位置的值,只有我们查询完以后才知道。因此我们可以采用分治FFT的方法,先递归下面,然后算下对上的贡献,最后递归上面。

    关键代码:

    inline void sol(int U, int D) {
    	if (U == D)	return ;
    	int mid = (U + D) >> 1;
    	sol(mid + 1, D);
    	++nwtime;
    	int Tot = 0;
    	for (register int j = m; j; --j) {
    		for (register int i = U; i <= mid; ++i) {
    			if (T[h[i][j]] != nwtime) { T[h[i][j]] = nwtime, sum[h[i][j]] = 0; }
    			f[i][j] = f[i][j] + Tot - sum[h[i][j]];
    			if (f[i][j] >= P)	f[i][j] -= P;
    			if (f[i][j] < 0)	f[i][j] += P;
    		}
    		for (register int i = mid + 1; i <= D; ++i) {
    			if (T[h[i][j]] != nwtime) { T[h[i][j]] = nwtime, sum[h[i][j]] = 0; }
    			ADD(Tot, f[i][j]);
    			ADD(sum[h[i][j]], f[i][j]);
    		}
    	}
    	sol(U, mid);
    }
    

    习题

    动态逆序对

    • 提示:三维为标号顺序,时间,大小关系

    [Violet]天使玩偶/SJY摆棋子

    • 提示:最值树状数组;三维为时间,横坐标,纵坐标

    Part 2:整体分治

    如果说 CDQ 分治是基于时间的分治算法,那么整体分治实际上就是基于值域的整体分治算法

    具体讲解lyd书上已经讲得很不错了。这里说一下我的看法:

    整体分治的一个经典应用为带(单点)修改的同时查询区间第k大。我们把操作离线下来,就变成了“支持单点修改的区间第k大”。由于要保证时间的关系,因此不能将其它的重新排序来搞事情。

    CDQ 分治的做法是二分时间,递归子问题,然后右面累加上左面的贡献。整体分治可以理解为:二分值域,先搞定左面对右面的贡献,然后一块递归子问题去求解。类似权值线段树求第k大,“搞定左对右的贡献”的方法就是 (k -= tmp),其中 (tmp) 为小于 (mid) 的元素个数,这个要用树状数组查询。由于是暂仅考虑左面(小者)对右面(大者)的贡献,因此树状数组维护的是比 (mid) 小的位置的权值树状数组。感觉越说越乱

    细节还是看代码吧

    代码较丑,说一下易错点:

    1. 数组大小要开三倍!!(因为每个查询操作可能会带来俩操作)

    2. 注意真的修改 (a) 数组的值!!

    例题:

    P2617 Dynamic Rankings:动态区间第 (k)

    题意同上。

    做法:1.离线 2.对值域二分,将离线下来的一系列操作按照值域分成两组,递归解决

    struct operations {
    	int type, x, y, z, id;
    	//两种可能。type=1/-1:x(值)y(useless)k(useless)id(位置)	(add/delete)
    	//type = 0: x(left)y(right)k(k)id(位置)	(query)
    }opts[N], ql[N], qr[N];
    void sol(int L, int R, int st, int ed) {
    	if (st > ed)	return ;
    	if (L == R) {
    		for (register int i = st; i <= ed; ++i)
    			if (opts[i].type == 0)
    				ans[opts[i].id] = L;
    		return ;
    	}
    	int ltop = 0, rtop = 0, mid = (L + R) >> 1;
    	for (register int i = st; i <= ed; ++i) {
    		if (opts[i].type == 0) {
    			int tmp = q(opts[i].y) - q(opts[i].x - 1);
    			if (opts[i].k <= tmp)	ql[++ltop] = opts[i];
    			else	opts[i].k -= tmp, qr[++rtop] = opts[i];
    		} else {
    			if (opts[i].x <= mid)	ad(opts[i].id, opts[i].type), ql[++ltop] = opts[i];
    			else	qr[++rtop] = opts[i];
    		}
    	}
    	for (register int i = st; i <= ed; ++i)
    		if (opts[i].type != 0)
    			tre_clear(opts[i].id);
    	for (register int i = 1; i <= ltop; ++i)
    		opts[st - 1 + i] = ql[i];
    	for (register int i = 1; i <= rtop; ++i)
    		opts[st - 1 + ltop + i] = qr[i];
    	sol(L, mid, st, st + ltop - 1);
    	sol(mid + 1, R, st + ltop, ed);
    }
    ...
    //main()
    for (register int i = 1; i <= n; ++i) {
    	read(a[i]);
    	opts[++otot] = (operations){1, a[i], 0, 0, i};
    }
    char opt[10];
    for (register int i = 1; i <= m; ++i) {
    	scanf("%s", opt);
    	if (opt[0] == 'Q') {
    		read(aa); read(bb); read(cc);
    		opts[++otot] = (operations){0, aa, bb, cc, ++atot};
    	} else {
    		read(aa); read(bb);
    		opts[++otot] = (operations){-1, a[aa], 0, 0, aa};
    		a[aa] = bb;
    		opts[++otot] = (operations){1, a[aa], 0, 0, aa};
    	}
    }
    sol(1, ltot, 1, otot);
    

    P3527 [POI2011]MET-Meteors:整体分治算法的优化

    题意:维护长为 (n) 的带颜色序列,支持 (m) 次区间加(非负数)。最后查询每种颜色最早在什么时候权值总和达到了 (p_i)

    (1 <= n, m <= 3e5)

    整体二分的扩展应用。

    考虑到整体分治算法实际上是对二分算法的一个优化,相当于把一堆二分任务一块完成,因此我们最好先想出二分暴力 (O(nmlogn)) 的算法,再想法优化到 (O(nlog^2n))

    二分的算法不难想出,因而整体分治的算法也不难想出。一种简便的处理 "NIE" 的方法为将总时间设置为 (m + 1),且第 (m + 1) 次设置为全部加正无穷,这样答案为 (m + 1) 实际上就是 "NIE"。

    随便敲了个整体分治,然而TLE了 (尽管后来卡常卡过去了) 。毕竟整体二分是 (O(nlog^2n)) 的,对于 (3e5) 的数据不是很好卡。

    这里介绍一种优化方法

    考虑到每种“时间”唯一对应一步操作,如果我们把它们也当作真的“操作”,每次都加入删除的话,比较费时间。不妨我们动态地加删,用什么就留什么。

    具体来说,就是维护指针 (nwt) 表示现在 (nwt) 及其之前的时间的操作都累加上了, (nwt) 以后的操作还没有累加。我们通过不停的调整 (nwt),使得树状数组里面存的是我们想要的那个时间区间(1~mid)。这样就可以省去清空树状数组的时间。(实际上就是动态清空)

    值得注意的是,如果用这种优化方法的话,我们就不用再每次对真“操作”的 (k) 都减去 (sum) 了,因为我们的 (sum) 是真的所有小于 (mid) 的和,而不是区间内部小于 (mid) 的和。

    (Code:)

    int nwt;
    void sol(int L, int R, int st, int ed) {
    	if (st > ed)	return ;
    	if (L == R) {
    		for (register int i = st; i <= ed; ++i)
    			ans[opts[i].id] = L;
    		return ;
    	}
    	int ltop = 0, rtop = 0, mid = (L + R) >> 1;
    	while (nwt < mid)	nwt++, change(nwt, c[nwt]);//<= mid : add
    	while (nwt > mid)	change(nwt, -c[nwt]), nwt--;//> mid : delete
    	for (register int i = st; i <= ed; ++i) {
    		ll sum = 0;
    		int id = opts[i].id;
    		for (register int j = 0; j < stat[id].size(); ++j) {
    			sum += q(stat[id][j]);
    			if (sum >= opts[i].x) {
    				sum = opts[i].x;
    				break;
    			}
    		}
    		if (opts[i].x <= sum)	ql[++ltop] = opts[i];
    		else	qr[++rtop] = opts[i];
    	}
    	for (register int i = 1; i <= ltop; ++i)
    		opts[st - 1 + i] = ql[i];
    	for (register int i = 1; i <= rtop; ++i)
    		opts[st - 1 + ltop + i] = qr[i];
    	sol(L, mid, st, st + ltop - 1);
    	sol(mid + 1, R, st + ltop, ed);
    }
    

    P1527 [国家集训队]矩阵乘法:二维数组的整体分治算法

    题意:矩阵中询问子矩阵的第 k 大。

    除了树状数组改为二维以外,无特殊的地方。同普通的整体分治求第k大。

    介绍一种错误算法:

    inline void tre_clear(int cur_x, int cur_y) {
    	for (register int i = cur_x; i <= n; i += lowbit(i))
    		for (register int j = cur_y; j <= n; j += lowbit(j)) {
    			//if (!tre[i][j])	return ;
    			tre[i][j] = 0;
    		}
    }
    

    将注释掉的代码加上将导致WA。但是不知道原因。可能原因比较复杂,尽量避免就好。

    (Code:)my record

  • 相关阅读:
    面向对象和面向过程的区别
    k-means算法
    win10系统下安装mysql
    python并发编程之多进程
    操作系统的概念
    前端基础之html
    聚类分析
    决策树
    Mysql
    SQL练习题
  • 原文地址:https://www.cnblogs.com/JiaZP/p/13299066.html
Copyright © 2020-2023  润新知