NOIP 备考日记 Day2

摩基亚

解析

不会 \(KD-tree\),那只好写 \(cdq\) 分治。
首先降维,我们把原本的询问中的矩形的四个顶点用差分或者说容斥,也就是二位前缀和的思想拆成四个左下角恒为原点的矩形,那么我们只用一个二维的坐标就能表示出这样的一个矩形。
我们只关注右上角的坐标,显然 \((x1 - 1, y1 - 1),(x2, y2)\) 对原本答案的贡献为正,\((x1 - 1, y2), (x2, y1 - 1)\) 对答案的贡献为负。
现在考虑如何求出这些矩形的答案。
我们考虑 \(cdq\) 中左半部分怎么对右半部分造成贡献。
显然只有时间在前,修改的位置在询问操作拆出来的矩形内部的点才能对这个询问造成贡献。
而满足在矩形内部也就是满足 \(x_l \leq x_r \ and \ y_l \leq y_r\),因为在边上的点我们也算,所以可以取等。
在最开始,保存这些信息的数组内部的时间顺序是严格递增的。
因为我们 \(cdq\) 时,是从小区间递归回到大区间,所以每次分治开始时,左区间每个操作的时间都是小于右区间的。
这样就省去了时间维。
我们再通过分别对左右两区间的以横坐标为第一关键字,纵坐标为第二关键字从小到大排序。
这样就保证了在单个区间内的横坐标是不降的,我们用两个指针在左右区间内移动,保证右边当前询问的点的横坐标大于左边的点,这样又省去了横坐标这一维。
那么我们只用考虑纵坐标这一维。
为了便于快速维护前缀和,我们用树状数组来维护修改操作造成的影响。
在保证了时间维单增,横坐标维不降的前提下,修改操作只会对纵坐标大于等于其修改的纵坐标的询问造成贡献,所以我们在树状数组中的这一位置加上其操作即可。
对于询问,我们直接在树状数组中查询即可,在我们前面操作的前提下,树状数组中保存的值都是能对该询问造成贡献的值。
在将该询问的贡献计入它对应的原本询问中即可。
最后清空树状数组。
因为这道题中的坐标会出现零,对于树状数组的 \(add\) 操作,会陷入死循环,所以我们让横纵坐标同时加一就可以避免复杂且易错的特判。

代码

#include<cstdio>
#include<cctype>
#include<algorithm>

using namespace std;

typedef long long LL;

const int N = 2e5 + 5, M = 2e6 + 5;

int opr, n, x1, y1, x2, y2, k, cnt = 0, tot = 0;

struct question { 
	int typ, x, y, id; LL val; 
	bool operator < (const question &a) {
		return x == a.x ? y < a.y : x < a.x;
	}
} q[N];

LL tr[M], ans[(int)1e4 + 5];

inline void add(int x, int k) { for(; x <= n; x += x & -x) tr[x] += k; }

inline LL ask(int x, LL res = 0) { for(; x; x -= x & -x) res += tr[x]; return res; }

void cdq(int l, int r) {
	if(l == r) return ;
	int mid = (l + r) >> 1;
	cdq(l, mid); cdq(mid + 1, r);
	sort(q + l, q + 1 + mid);
	sort(q + mid + 1, q + 1 + r);
	int i = l, j = mid + 1;
	for( ; j <= r; j++) {
		while(q[i].x <= q[j].x && i <= mid) {
			if(!q[i].id) add(q[i].y, q[i].val);
			i++;
		}
		if(q[j].id) ans[q[j].id] += q[j].typ * ask(q[j].y);
	}
	for(j = l; j < i; j++) if(!q[j].id) add(q[j].y, -q[j].val);
}

inline void read(int &x) {
	x = 0; int c = getchar();
	for(; !isdigit(c); c = getchar());
	for(; isdigit(c); c = getchar())
		x = x * 10 + c - 48;
}

void write(LL x) {
	if(x > 9) write(x / 10);
	putchar(x % 10 + '0');
}

int main() {
	read(opr), read(n);
	while(1) {
		read(opr);
		if(opr == 3) break;
		if(opr == 1) {
			read(x1), read(y1), read(k);
			q[++cnt] = (question) { 0, x1 + 1, y1 + 1, 0, k };
		}
		else {
			read(x1), read(y1), read(x2), read(y2);
			q[++cnt] = (question) { 1, x2 + 1, y2 + 1, ++tot, 0 };
			q[++cnt] = (question) { 1, x1, y1, tot, 0 };
			q[++cnt] = (question) { -1, x2 + 1, y1, tot, 0 };
			q[++cnt] = (question) { -1, x1, y2 + 1, tot, 0 };
		}
	}
	n += 1; cdq(1, cnt);
	for(int i = 1; i <= tot; i++) write(ans[i]), putchar('\n');
	return 0;
}

MET-Meteors

答案满足左闭右开的单调性,所以对于单个询问我们是可以二分的。
但是对于多个询问一个一个的二分的时间是我们无法接受的,所以我们考虑整体二分。
我们二分答案区间 \(l, r\) 和询问区间 \(x, y\)
我们让答案左区间的陨石全部落下,然后把询问区间分成两类。
一类是左区间的陨石能够满足它的需求,另一类是不能满足其需求。
对于第一类,显然它的答案有更优的潜能,我们将其递归到左区间进行处理,对于第二类,我们把它的需求减去左区间的陨石量(这很显然吧,不懂可以类比在值域线段树中二分第 \(k\) 小的值的过程),它的答案显然大于 \(mid\),所以我们将这一部分递归到右区间进行处理。
用断环成链来处理环形,用树状数组加差分来维护区间加减小常数。
对于是否满足其需求,我们枚举这个询问的太空站的位置,将它的贡献(两部分:环的一半和另一半)累加起来,然后判断一下总的陨石雨的量与它的需求量的大小关系就好了。

#include<cstdio>
#include<cctype>
#include<vector>

using namespace std;

typedef long long LL;

const int N = 3e5 + 5, inf = 1e9;

int n, m, k; LL ans[N], tr[N << 1];

struct data { int ned, id; } a[N], a1[N], a2[N];

struct opr { int l, r, v; } b[N], b1[N], b2[N];

vector < int > s[N];

inline void add(int x, int k) { for(; x <= m << 1; x += x & -x) tr[x] += k; }

inline LL ask(int x, LL res = 0) { for(; x; x -= x & -x) res += tr[x]; return res; }

void solve(int l, int r, int x, int y) {
	if(x > y) return ;
	if(l == r) {
		for(int i = x; i <= y; i++) ans[a[i].id] = l;
		return ;
	}
	int mid = (l + r) >> 1, cnt1 = 0, cnt2 = 0;
	for(int i = l; i <= mid; i++) add(b[i].l, b[i].v), add(b[i].r + 1, -b[i].v);
	for(int i = x; i <= y; i++) {
		LL tmp = 0;
		for(unsigned int j = 0; j < s[a[i].id].size() && tmp < a[i].ned; j++)
			tmp += ask(s[a[i].id][j]) + ask(s[a[i].id][j] + m);
		if(tmp >= a[i].ned) a1[++cnt1] = a[i];
		else a[i].ned -= tmp, a2[++cnt2] = a[i];
	}
	for(int i = l; i <= mid; i++) add(b[i].l, -b[i].v), add(b[i].r + 1, b[i].v);
	for(int i = x; i <= x + cnt1 - 1; i++) a[i] = a1[i - x + 1];
	for(int i = x + cnt1; i <= y; i++) a[i] = a2[i - x - cnt1 + 1];
	solve(l, mid, x, x + cnt1 - 1); solve(mid + 1, r, x + cnt1, y);
}

inline void read(int &x) {
	x = 0; int c = getchar();
	for(; !isdigit(c); c = getchar());
	for(; isdigit(c); c = getchar())
		x = x * 10 + c - 48;
}

int main() {
	read(n), read(m);
	for(int i = 1, x; i <= m; i++) read(x), s[x].push_back(i);
	for(int i = 1; i <= n; i++) read(a[i].ned), a[i].id = i;
	read(k);
	for(int i = 1; i <= k; i++) {
		read(b[i].l), read(b[i].r), read(b[i].v);
		if(b[i].l > b[i].r) b[i].r += m;
	}
	b[++k] = (opr) { 1, m << 1, inf };
	solve(1, k, 1, n);
	for(int i = 1; i <= n; i++)
		if(ans[i] == k) printf("NIE\n"); else printf("%lld\n", ans[i]);
	return 0;
}

二分图 /【模板】线段树分治

解析

根据二分图的顶点可以分为两个内部的点之间没有连边的非空集合的定义,我们直接用并查集维护每个点所属的集合即可,如果出现一条边的两个端点同时属于一个集合,那么这个图就不是二分图。
但是现在每条边都多了一个时间维。
我们对时间维进行分治。
过程类似于线段树建边的过程,线段树上的区间表示时间,我们把一条在 \(l, r\) 时间内存在的边以类似线段树区间加的方式加入到线段树上的节点中。
在分治的过程中,我们依次连接当前区间的边,并用并查集判断能否形成二分图,如果不能,那么整个区间肯定不能,直接输出 \(No\)
如果能,我们再递归进入左右子区间,继续将左右子区间的边加入到图中,因为左右子区间的边是不存在于当前区间的,所以左右子区间的限制更高,所以直到递归到叶子节点时,该图还是二分图,我们才能说 \(Yes\)
在递归结束后,我们还要撤销当前区间的并查集合并操作。
这样用采用路径压缩的并查集在时间复杂度上是假的,为了保证查询和撤销的时间复杂度,我们采用按秩合并。
因为该题在某个时刻是先询问,后出现边,所以边的出现时间只会影响 \(l + 1, r\) 这个时间段。

代码

#include<cstdio>
#include<cctype>
#include<vector>
#include<algorithm>

using namespace std;

const int N = 1e5 + 5;

int n, m, k, fa[N << 1], rk[N << 1], top = 0;

struct edge { int x, y; } e[N << 1];

vector < int > tr[N << 2];

void update(int p, int l, int r, int L, int R, int x) {
	if(L <= l && r <= R) return tr[p].push_back(x);
	int mid = (l + r) >> 1;
	if(L <= mid) update(p << 1, l, mid, L, R, x);
	if(R >  mid) update(p << 1 | 1, mid + 1, r, L, R, x);
}

struct stack { int x, add; } sta[N << 1];

inline int Find(int x) { while(x != fa[x]) x = fa[x]; return x; }

inline void merge(int x, int y) {
	x = Find(x), y = Find(y);
	if(x == y) return ;
	if(rk[x] > rk[y]) swap(x, y);
	sta[++top] = (stack) { x, rk[x] == rk[y] };
	fa[x] = y;
	if(rk[x] == rk[y]) ++rk[y];
}

void solve(int p, int l, int r) {
	int f = 1, las = top;
	for(unsigned int i = 0; i < tr[p].size(); i++) {
		int x = e[tr[p][i]].x, y = e[tr[p][i]].y;
		int fx = Find(x), fy = Find(y);
		if(fx == fy) {
			for(int j = l; j <= r; j++) printf("No\n");
			f = 0; break;
		}
		merge(x, y + n); merge(x + n, y);
	}
	if(f) {
		if(l == r) printf("Yes\n");
		else {
			int mid = (l + r) >> 1;
			solve(p << 1, l, mid);
			solve(p << 1 | 1, mid + 1, r);
		}
	}
	while(top > las) rk[fa[sta[top].x]] -= sta[top].add, fa[sta[top].x] = sta[top].x, top--;
}

inline void read(int &x) {
	x = 0; int c = getchar();
	for(; !isdigit(c); c = getchar());
	for(; isdigit(c); c = getchar())
		x = x * 10 + c - 48;
}

int main() {
	read(n), read(m), read(k);
	for(int i = 1, l, r; i <= m; i++) {
		read(e[i].x), read(e[i].y), read(l), read(r);
		update(1, 1, k, l + 1, r, i);
	}
	for(int i = 1; i <= n << 1; i++) fa[i] = i, rk[i] = 1;
	solve(1, 1, k);
	return 0;
}
posted @ 2021-09-07 15:05  init-神眷の樱花  阅读(36)  评论(0)    收藏  举报