归档 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
怎么又是环形问题,,,
因为超过一年半前(怎么我已经学了这么久的 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,字面意思,给异或用的哈希,专门应对这种情况。
给每个边权映射一个值,为 base
的 rand()
次方,自然溢出即可。
然后直接按照之前的操作处理就好了。
我看完过后很疑惑啊,这到底是什么玄学逻辑???
看完以后,知道了,如果一个你想找到类似于 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
—— · EOF · ——
真的什么也不剩啦 😖