归档 221009 - 221014 | 做题记录

事先声明,这不是题解,只是做题记录,所以如果你把它当做题解来看看不懂很正常。

你问我为什么跳了 A 题?因为摆了。

B. 萌萌哒 - 221009

https://www.luogu.com.cn/problem/P3295

是道好题,让我对倍增有了更深的理解。

很容易想到对于形如 \([l_1, r_1]\)\([l_2, r_2]\),只需让两个区间内每个数两两对应相等。

由相等关系易联想到并查集,依次合并后求根节点个数即可。

如果暴力合并整个区间的话,复杂度是 \(\mathcal O(n^2\alpha)\) 的,考虑优化。

然而并没有想到怎么优化(哭哭

瞅了一眼题解,可以用倍增???看不懂,摆了摆了

(221011 update)

好吧,事情仿佛和我想象的有一点出入。

如果我们要合并两个区间,等价于合并两个区间中 相对位置相同的、长度相等 的若干对子区间,满足这些子区间的并是原区间。

然后用一个倍增的并查集去维护。倍增大家都会吧,用 f[i][k] 表示 \([i,i+2^k-1]\) 的父亲就好。

对于合并操作,在层内合并即可。因为合并的区间可以相交,所以按照 ST 表查询的方式来打就行了。

最后统计 f[i][0] 中根的个数,明显要先将上层的关系先下传。将合并两个大区间的操作转化为合并四个小区间即可。

因为不想打按秩合并,所以时间复杂度 \(\mathcal O(\alpha n\log n)\)

namespace XSC062 {
using namespace fastIO;
const int maxm = 35;
const int mod = 1e9 + 7;
const int maxn = 1e5 + 5;
int f[maxm][maxn];
int n, m, siz, l1, l2, r1, r2, ans;
inline void Init(int s, int n) {
	for (int i = 0; i <= s; ++i) {
		for (int j = 1; j <= n; ++j)
			f[i][j] = j;
	}
	return;
}
int find(int s, int x) {
	return f[s][x] == x ? x : f[s][x] = find(s, f[s][x]);
}
inline void merge(int s, int x, int y) {
	f[s][find(s, x)] = find(s, y);
	return;
}
inline int qkp(int x, int y) {
	int res = 1;
	while (y) {
		if (y & 1)
			(res *= x) %= mod;
		(x *= x) %= mod;
		y >>= 1;
	}
	return res;
}
int main() {
	read(n), read(m);
	siz = log(n) / log(2.0) + 1;
	Init(siz, n);
	while (m--) {
		read(l1), read(r1);
		read(l2), read(r2);
		int k = log(r1 - l1 + 1.0) / log(2.0);
		merge(k, l1, l2);
		merge(k, r1 - (1ll << k) + 1, r2 - (1ll << k) + 1);
	}
	for (int i = siz; i; --i) {
		for (int j = 1; j <= n; ++j) {
			int x = find(i, f[i][j]);
			merge(i - 1, j, x);
			merge(i - 1, j + (1ll << (i - 1)),
						 x + (1ll << (i - 1)));
		}
	}
	for (int i = 1; i <= n; ++i)
		ans += (f[0][i] == i);
	ans = 9 * qkp(10, ans - 1);
	print(ans % mod);
	return 0;
}
} // namespace XSC062

C. 国旗计划 - 221011 to 221013

https://www.luogu.com.cn/problem/P4155

看来一个 B 题还不能让我理解倍增是什么。

怎么什么都能倍增啊???看来需要再开一个坑学倍增了(其实并没有系统学过倍增。。。菜

很明显链上的区间覆盖是一个入门的贪心,环形的大家也都会,把整个链复制一下就好。

那么接下来我们把这个东西倍增一下。

倍增什么呢?一般来说,做什么事的时候会 T 飞我们就倍增什么。

对于这道题,求 \(i\) 为起点的区间覆盖问题 的时候会 T 飞,因为区间覆盖问题是一直往右边选,所以就用 f[i][j] 表示以 \(i\) 为起点,往右边选 \(2^j\) 个区间时最右能到多远。

然后像 LCA 一样直接跑就行了吧,,,感觉后面没什么好写的。

upd on 221013

打之前思考了两天一个问题,预处理 f[i][0] 的时候,它不是一个 \(n^2\) 的东西吗???

然后思考了两天也没有思考出来,就直接打了,然后寄了。

for (int i = 1; i <= 2 * n; ++i) {
	int p = i + 1;
	while (p <= 2 * n && a[i].r >= a[p].l)
		++p;
	f[i][0] = p - 1;
}

……起码这说明我的判断没有问题,它确实是 \(n^2\) 的。

后来翻了很多篇题解都没有找到做法,最后在讨论区知道了。

排序后的 f[i][0] 是单调递增的!!我是小丑 🤡🤡🤡

所以可以不在每次循环的时候都从 \(i+1\) 开始找。

for (int i = 1; i <= 2 * n; ++i) {
	static int p = i + 1;
	while (p <= 2 * n && a[i].r >= a[p].l)
		++p;
	f[i][0] = p - 1;
}

最终时间复杂度 \(\mathcal O(n\log n)\)

namespace XSC062 {
using namespace fastIO;
const int maxm = 35;
const int maxn = 4e5 + 5;
struct _ { int l, r, i; };
_ a[maxn];
int ans[maxn]; 
int n, m, siz;
int f[maxn][maxm];
int main() {
	read(n), read(m);
	siz = log(2 * n) / log(2.0) + 1;
	for (int i = 1; i <= n; ++i) {
		read(a[i].l);
		read(a[i].r);
		a[i].i = i;
		if (a[i].r < a[i].l)
			a[i].r += m;
		a[n + i].i = i;
		a[n + i].l = a[i].l + m;
		a[n + i].r = a[i].r + m;
	}
	std::sort(a + 1, a + 2 * n + 1, [&](_ x, _ y) -> bool {
		return x.l < y.l; } );
	for (int i = 1; i <= 2 * n; ++i) {
		static int p = i + 1;
		while (p <= 2 * n && a[i].r >= a[p].l)
			++p;
		f[i][0] = p - 1;
	}
	for (int j = 1; j <= siz; ++j) {
		for (int i = 1; i <= 2 * n; ++i)
			f[i][j] = f[f[i][j - 1]][j - 1];
	} 
	for (int i = 1; i <= n; ++i) {
		int res = 1, t = i;
		for (int j = siz; ~j; --j) {
			if (f[i][j] && a[f[t][j]].r < a[i].l + m) {
				t = f[t][j];
				res += (1ll << j);
			}
		}
		ans[a[i].i] = res + 1;
	}
	for (int i = 1; i <= n; ++i)
		print(ans[i], ' ');
	return 0;
}
} // namespace XSC062

D. Wilcze doły - 221010

https://www.luogu.com.cn/problem/P3594

很好奇为什么洛谷上的题目名字和原名不一样,可能因为 ł 是个神秘的特殊字符。WIL 是啥???哦哦取的前三个字母啊那没事了。

因为都是正数,所以删最多,也就是说要删就得一次性删 \(d\) 个。

不难想到对于一个区间,肯定选择删掉区间内总和最大的连续 \(d\) 个数。

枚举右端点 \(i\),单调队列维护左右端点间总和最大的连续 \(d\) 个数。

问题来了,左端点怎么找?这是这道题的关键。需要想明白,\(i + 1\) 为右端点的区间的左端点 不可能比 \(i\) 为右端点的区间的左端点 更靠左。也就是说,左端点具有单调性

假设当前左端点为 \(l\),则当 \([l,i]\) 的总和减去最大的 \(d\) 个数的和后仍然大于 \(p\),就可以将 \(l\) 向后移动了。

namespace XSC062 {
using namespace fastIO;
const int maxn = 2e6 + 5;
int n, p, d, l, h, t, ans;
int a[maxn], s[maxn], q[maxn];
inline int max(int x, int y) {
	return x > y ? x : y; 
}
int main() {
	read(n), read(p), read(d);
	l = 1;
	q[h = t = 1] = d;
	for (int i = 1; i <= n; ++i) {
		read(a[i]);
		a[i] += a[i - 1];
		if (i >= d)
			s[i] = a[i] - a[i - d];
		if (i > d) {
			while (h <= t && q[h] - d + 1 < l)
				++h;
			while (h < t && a[i] - a[l - 1] - s[q[h]] > p) {
				while (h <= t && a[i] - a[l - 1] - s[q[h]] > p)
					++l;
				while (h <= t && q[h] - d + 1 < l)
					++h;
			}
			ans = max(ans, i - l + 1);
			while (h <= t && s[i] >= s[q[t]])
				--t;
			q[++t] = i;
		}
	}
	print(ans);
	return 0;
}
} // namespace XSC062

E. 旅行问题 - 221014

https://loj.ac/p/10178

怎么又是环形问题,,,

因为超过一年半前(怎么我已经学了这么久的 OI 了???)做过这道题,所以还有印象这是道单调队列。

对于以第 \(i\) 个站开头的行程,能否走完全程取决于 \(min\{\sum\limits_{k=i}^jp_k-\sum\limits_{k=i}^jd_k\}\)\(0\) 的大小关系。

所以我们直接套个单调队列维护 \(p\)\(d\),再注意一下分类讨论方向就好啦。不知道为什么没多少人做这道题。


牛客提高第四场 T1 - 221011 to 221012

https://www.becoder.com.cn/problem/47538

省流:给定一棵带权树,把 \((u,v)\) 间简单路径上的边权全部拉出来生成数组,两个人在数组中轮流选数,每次选走的数必须小于等于上一个人选走的数,不能选的人输,问有多少个 \((u,v)\) 满足先手必胜。

首先给一个结论,如果在 \((x, y)\) 的路径上有任何一种边权的数量是奇数,那么就要统计 \((x,y)\)

首先,假设 \(x\) 是一个边权。如果从最小边权一直到 \(x\),每个边权的出现次数之和为奇数,先手必胜。

因为这个时候先手选 \(x\),后手选任意一个小于等于 \(x\) 的边权,原来总共有奇数个可选项,选走两个,剩下的可选项的个数还是奇数个。

如此递归下去,最后一定会出现:可选最小边权剩下奇数个,那么两个轮流选,明显先手胜。

然后,如果任意一种边权出现的次数是奇数,那么先手胜。

为什么呢?因为如果它是最小的出现次数为奇数的边权,那么它和比它小的所有边权的数量之和为奇数,因为前面的出现次数都是偶数。如果它不是最小的,那么明显就是因为存在比它更小的。根据我们刚刚得到的结论,此时先手胜。

所以,问题就转化为了:统计点对 \((x, y)\) 的数量,满足 \(x\)\(y\) 的简单路径中存在出现次数为奇数的边权。

另一种可行的转化是,用 \(C_n^2\) 减去路径上不存在出现次数为边权的点对数。

一些鲜花

我和温柔可爱善良贤淑窈窕少女 cqbztzl 都想到了这个简单的题意转化,但是可怜的菌菌没有。这波啊,这波我和智力在平流层。

然后我就打了一个每个点到根节点的路径总异或值,交上去过后智力告诉我这是萎的,然后用一个简单的例子 Hack 了我,我感到慌张。

然后智力得意地炫耀自己有先见之明打了 bitset,然后我向他展示了一下聊天记录。


18:56 OL【可爱少女智力征婚中】

智力

18:56 OL【可爱少女智力征婚中】

bitset 的时空都是 1/32 对吧

18:57 OL【可爱少女智力征婚中】

那我有救了 XD


智力,你以为你想得到的我想不到吗!!!

结果是最后智力的分比我低,笑嘻了我直接。

接下来讲讲统计。

我一开始的想法是,统计根节点到每个点上的路径的异或值(记为 f),然后若 f[x]f[y] 相等,说明 \(x\)\(y\) 的路径上异或起来是 \(0\),全部出现了偶数次。

后来智力找到我,用 1 ^ 2 ^ 3 = 0 这个例子 Hack 了我。并不是说异或起来为 \(0\) 就是相等的了。

后来瞄了一眼题解,是新科技!!!

我承认我平常 abc 摆太多场了,居然连这种优秀的新科技都不知道!!!看来以后要多摆!!!

这个要用到一个 xor-hashing,字面意思,给异或用的哈希,专门应对这种情况。

给每个边权映射一个值,为 baserand() 次方,自然溢出即可。

然后直接按照之前的操作处理就好了。

我看完过后很疑惑啊,这到底是什么玄学逻辑???

看完以后,知道了,如果一个你想找到类似于 1 ^ 2 ^ 3 = 0 的情况,其出现概率与数字的二进制位数有关。因为 xor 只针对于同一位,结果不会被上一位或下一位干扰,所以每一位出现异或起来为 \(0\) 的概率是 \(\dfrac 12\)。只要我们整点比较强力的 \(k\) 位二进制数,那么出现以上情况的概率就是 \(2^{-k}\)

那么这个比较强力的 \(k\) 位二进制数,用比较强力的类字符串哈希生成方式,再使用一个很大很大的随机数替代字符串哈希中表示下标的 \(i\),用自然溢出让它显得更加稳妥就好。

所以现在我们程序寄掉的概率就是 \(\dfrac 1{2^{64}}\),好事情啊好事情。

最后时间复杂度 \(\mathcal O(n\log n)\)\(\log n\) 来源于映射。

namespace XSC062 {
using namespace fastIO;
const int _p = 13331;
const int maxn = 5e5 + 5;
struct _ {
	int v;
	ull w;
	_ () {}
	_ (int v1, ull w1) {
		v = v1, w = w1;
	}
};
ull w;
ull f[maxn];
int T, n, x, y, ans;
std::map<ull, int> t;
std::map<ull, ull> q;
std::vector<_> g[maxn];
inline void Init(int n) {
	t.clear();
	q.clear();
	for (int i = 1; i <= n; ++i) {
		f[i] = 0;
		g[i].clear();
		g[i].shrink_to_fit();
	}
	return;
}
void DFS(int x, int fa) {
	++t[f[x]];
	for (auto i : g[x]) {
		if (i.v == fa)
			continue;
		f[i.v] = f[x] ^ i.w;
		DFS(i.v, x);
	}
	return;
}
inline void add(int x, int y, ull w) {
	g[x].push_back(_(y, w));
	return;
}
inline ull randint(void) {
	ull res = rand();
	res *= rand();
	res *= rand();
	return res;
}
inline ull qkp(ull x, ull y) {
	ull res = 1;
	while (y) {
		if (y & 1)
			res *= x;
		x *= x;
		y >>= 1;
	} 
	return res;
}
int main() {
	read(T);
	srand(time(NULL));
	while (T--) {
		read(n);
		Init(n);
		ans = n * (n - 1) / 2;
		for (int i = 1; i < n; ++i) {
			read(x), read(y), read(w);
			if (!q.count(w))
				q[w] = qkp(_p, randint());
			w = q[w];
			add(x, y, w), add(y, x, w);
		}
		DFS(1, -1);
		for (auto i : t)
			ans -= i.second * (i.second - 1) / 2;
		print(ans, '\n');
	}
	return 0;
}
} // namespace XSC062
posted @ 2022-10-10 13:26  XSC062  阅读(50)  评论(1编辑  收藏  举报