由 [Ynoi 2016] 镜中的昆虫 引发的一系列题目

前置知识: 珂朵莉树与 CDQ 分治。

CDQ 分治

先来介绍一下 CDQ 分治:

看看 CDQ 能解决什么问题:

  • 解决和点对有关的问题。
  • 1D 动态规划的优化与转移。
  • 通过 CDQ 分治,将一些动态问题转化为静态问题。

例题 1:P4390

然而,比较常用的是第三种,我们可以看一道例题 P4390

大概就是给一个矩阵,单点修改,矩形求和。先将 \((x_1,y_1)\)\((x_2,y_2)\) 的矩形求和转化为差分形式,即 \(sum[x_2][y_2]-sum[x_2][y_1-1]-sum[x_1-1][y_2]+sum[x_1-1][y_1-1]\)

这样就把一个问题拆成了四个问题。这样更利于 CDQ 处理。因为有修改操作,我们加一维时间 \(t_i\),这样对于每个点就三个维度 \(x_i,y_i,t_i\)。当然,每个点也有权值 \(v_i\)。我们将按照读入顺序从小到大去赋值 \(t_i\)。比如询问 \(sum[x][y]\),且当前时间为 \(t\),就可以转化为 \(\sum_{i=1}^{n} v_i[x_i\leq x,y_i\leq y, t_i\leq t]\)。这就是模板三维偏序了。

代码:

struct edge {
	int x, y, id, val, sign; 
	// id 表示是第几个询问
	// val是权值,询问的时候没有权值
	// sign 代表这个询问的符号,sign=0表示是修改
}ed[N], q[N];

void merge_sort(int l, int r) {
	if (l >= r) return;
	int mid = (l + r) >> 1;
	merge_sort(l, mid), merge_sort(mid + 1, r);
	int i = l, j = mid + 1, cnt = 0;
	while (i <= mid && j <= r) {
		if (ed[i].x <= ed[j].x) tr.modify(ed[i].y, ed[i].val), q[++cnt] = ed[i], i++;
		else ans[ed[j].id] += tr.query(ed[j].y) * ed[j].sign, q[++cnt] = ed[j], j++;
	}
	while (i <= mid) tr.modify(ed[i].y, ed[i].val), q[++cnt] = ed[i], i++;
	while (j <= r) ans[ed[j].id] += tr.query(ed[j].y) * ed[j].sign, q[++cnt] = ed[j], j++;
	for (int i = l; i <= mid; i++) tr.modify(ed[i].y, -ed[i].val);
	for (int i = l, j = 1; i <= r; i++, j++) ed[i] = q[j];
} 

signed main() {
	cin >> op >> n;
	while (cin >> op) {
		if (op == 3) break;
		int x1, y2, x2, y1, x; 
		if (op == 1) {
			cin >> x1 >> y2 >> x;
			ed[++tot] = {x1, y2, 0, x, 0};  
		} 
		else {
			idx++;
			cin >> x1 >> y1 >> x2 >> y2;
			ed[++tot] = {x2, y2, idx, 0, 1};
			ed[++tot] = {x2, y1 - 1, idx, 0, -1};
			ed[++tot] = {x1 - 1, y2, idx, 0, -1};
			ed[++tot] = {x1 - 1, y1 - 1, idx, 0, 1};
			// 在这里,t_i 我们不需要真正开出来,我们按照输入顺序依次加入点对,就保证了 ti 从小到大
		}
	} 
	merge_sort(1, tot);
	for (int i = 1; i <= idx; i++) cout << ans[i] << endl;
}

例题 2:动态逆序对

再看一道例题:动态逆序对

这道题要先转化一下,我们倒着来考虑,从逆序对 \(0\) 开始,倒着加入每个数。我们只需要计算加入这个数产生的贡献就行了。那么考虑 \(t_i\):我们倒着加,时间从 \(1\) 开始(相当于正着来时间是从大到小排列),那么加入一个数 \(x\),位置为 \(y\),且倒着时间为 \(t\) 的贡献为:\(t_i<t,x_i>x,y_i<y\) 或者 \(t_i<t,x_i<x,y_i>y\) 的数量。可以用 CDQ 分治解决。

例题 3:CF848C

咳咳有点偏题了。我们再讲讲 [Ynoi 2016] 镜中的昆虫 的弱化版 CF848C

发现 最后一次出现位置的下标减去第一次出现位置的下标 这个东西十分鬼畜,但是我们把他转化一下,就是把他拆开,比如数列 3 4 3 4 3 3,计算 \(3\) 的话贡献为 \(6-1=5\),这是十分难维护的,我们可以这样:\((3-1)+(5-3)+(6-5)=5\),即我们只需要维护每个数的前驱位置。那么 \([1,r]\) 的贡献就转化为 \(\sum_{i=l}^{r} i-pre_i[pre_i\geq l]\),其实就是 \(\sum _{i=1}^{r} i-pre_i[pre_i\geq l]\)。我们把 \((t,x,y)\) 看成数对,\(x\) 是现在的下标,\(y\)\(x\) 的前驱,权值就是 \(x-y\)。这个形式非常的三维偏序,就是找到所有的 \(t_i\leq t,x_i\leq r,y_i\geq l\) 就行。

修改的时候:set 维护每个颜色出现的位置,然后用这个找前驱后继。修改时会删除一些前驱关系,也会增加一些前驱关系,这样可增加个符号,删除乘上 \(-1\),增加乘上 \(1\)。询问的话,加入数对 \([t,r,l]\) 即可。

const int N = 1e6 + 5;

int n, m, a[N], tot, ans[N];
set<int> s[N];

struct edge {
	int x, y, id, f;
	int val() {
		return f * (x - y);
	}
}ed[N], w[N];

struct fenwick {
	int c[N];
	void modify(int x, int v) {
		for (int i = x; i <= N - 5; i += i & -i) c[i] +=  v;
	}
	int query(int x) {
		int res = 0;
		for (int i = x; i; i -= i & -i) res += c[i];
		return res;
	}
}tr;

void merge_sort(int l, int r) {
	if (l >= r) return;
	int mid = (l + r) >> 1;
	merge_sort(l, mid), merge_sort(mid + 1, r);
	int i = l, j = mid + 1, cnt = 0;
	while (i <= mid && j <= r) {
		if (ed[i].x <= ed[j].x) tr.modify(ed[i].y, ed[i].val()), w[++cnt] = ed[i], i++;
		else ans[ed[j].id] += tr.query(n) - tr.query(ed[j].y - 1), w[++cnt] = ed[j], j++;
	}
	while (i <= mid) tr.modify(ed[i].y, ed[i].val()), w[++cnt] = ed[i], i++;
	while (j <= r) ans[ed[j].id] += tr.query(n) - tr.query(ed[j].y - 1), w[++cnt] = ed[j], j++;
	for (int i = l; i <= mid; i++) tr.modify(ed[i].y, -ed[i].val());
	for (int i = l, j = 1; i <= r; i++, j++) ed[i] = w[j];
}

signed main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = 1; i <= n; i++) {
		s[a[i]].insert(i);
		if (s[a[i]].size() > 1) ed[++tot] = {i, *(--(--s[a[i]].end())), 0, 1};
	}
	int idx = 0;
	for (int i = 1; i <= m; i++) {
		int op, x, y;
		cin >> op >> x >> y;
		if (op == 2) ed[++tot] = {y, x, ++idx, 0};
		else {
			auto it = s[a[x]].find(x);
			int L = 0, R = 0;
			if (it != s[a[x]].begin()) ed[++tot] = {x, *--it, 0, -1}, L = *it, it++;
			if (++it != s[a[x]].end()) ed[++tot] = {*it, x, 0, -1}, R = *it;
			if (L && R) ed[++tot] = {R, L, 0, 1};
			s[a[x]].erase(x);
			
			a[x] = y; s[a[x]].insert(x);
			L = 0, R = 0, it = s[y].find(x);
			if (it != s[y].begin()) ed[++tot] = {x, *--it, 0, 1}, L = *it, it++;
			if (++it != s[y].end()) ed[++tot] = {*it, x, 0, 1}, R = *it;
			if (L && R) ed[++tot] = {R, L, 0, -1};
		}
	}
	merge_sort(1, tot);
	for (int i = 1; i <= idx; i++) cout << ans[i] << endl;
}

回归正文

回到 [Ynoi 2016] 镜中的昆虫 这道题,这道题询问区间不同数的数量,我们仿照上文讲过的思路依然存储 \((t,x,y)\)\(x\) 是现在的下标,\(y\)\(x\) 的前驱,然后询问 \([l,r]\) 就是询问有多少个数的前驱小于 \(l\)。(本质上就是只记录第一个出现的数)。

但是这道题,是区间修改,不能一个个修改对吧?但是,我们只需要看那些变化的 \(pre_i\),可能变化的 \(pre_i\) 没有想象的那么多。什么点 \(pre_i\) 会变化:区间中每个颜色段的左端点,这些点在它们的颜色里的后继的左端点,修改的段在新的颜色里的后继的左端点。可以证明:\(pre_i\)变化次数为 \(O(n+m)\) 级别,所以如果能很快找出这些点,复杂度完全可以保证。怎么证明这个结论,读者自证不难。

怎么找出这些点,我们要维护连续段。这就要用到珂朵莉树了。

posted @ 2024-03-02 20:00  Otue  阅读(6)  评论(0编辑  收藏  举报