8_2 模拟赛_ios看大家都写了也就来水一篇。



8_2 模拟赛_复杂度_02

写在前面

看大家都写了也就来水一篇。

不算很难 \(\ldots\)​ 也不知道模拟赛的难度到底是怎么搞的,有的时候特别高有的时候就比较低。

T1

题意:

给定树,树会发生变换,具体的,每个点的父亲会变成 \((f_x + 19940105 \bmod x - 1 ) + 1\)​,一共变 \(9\)​ 次,请你输出 \(10\)​ 次里每一次的所求,具体而言,如果这个树可以被大小为 \(x\)​ 的连通块恰好覆盖,则输出 \(x\)​,否则不输出,要求输出按升序。

\(n \le 1.2 \times 10^6\),时间限制 1000ms,空间限制 128M。

\(\rm Analysis:\)

这道题似乎是有 ​​原题​​ 的,这题有一个很显然的做法,首先可能的 \(x\)​ 必然是约数,然后对于所有 \(x\)​​ 树形 dp 即可,如果某一个点的 size 恰为 x,那么就删除这个点作为一个块,否则贡献到父亲,如果有一个点的 size 超过 x,那么这时必然不行了,这个复杂度为 \(\cal O\rm(10 \times \sigma_0(n) \times n)\),无法通过。

从这个 dp 中,我们得到一个结论,可行的树必然有超过 \(\lfloor\cfrac{n}{x}\rfloor\) 个点的原 size 是 \(x\)​​ 的倍数,因为删除的地方就是倍数,至少有那么多,否则删不完。

那么考虑是否只要有这么多,就必然可行,考虑根一定是,那么从根必然可以拿出一个 \(x\) 的子树,且不影响其他关键点,递归下去可以得到必然可以划分,于是可行等价于有超过 \(\lfloor \cfrac{n}{x}\rfloor\) 个点 size 为 \(x\) 的倍数。

于是可以只用一遍 dfs 求出 size,然后对于每个约数暴力枚举倍数,看有多少个,那这个复杂度是约数和,筛一下发现,珂以通过,或者有点梦想,相信是正解就完事了。不过这玩意卡 dfs 递归常数,由于父亲的编号小于自己,所以可以一次循环解决。

#include <bits/stdc++.h>

signed main() {
// std::ios::sync_with_stdio(false);
// std::cin.tie(nullptr);

int n;
std::cin >> n;
std::vector<int> fa(n + 1);
scanf("%d", &fa[2]);
for (int i = 3; i <= n; ++i) {
scanf(",%d", &fa[i]);
}

std::vector<int> siz(n + 1, 1), tot(n + 1);

std::function<int(int)> solve = [&](int d) -> int {
int res = 0;
for (int i = d; i <= n; i += d) res += tot[i];
return res >= (n / d);
};

std::vector<int> d;
printf("Case #1:\n");
printf("1\n");

for (int i = n; i; --i) siz[fa[i]] += siz[i];
for (int i = 1; i <= n; ++i) ++tot[siz[i]];

for (int i = 2; i < n; ++i) {
if (!(n % i)) {
int flg = solve(i);
if (flg) printf("%d\n", i);
d.emplace_back(i);
}
}

printf("%d\n", n);

int T = 9;
while (T--) {
for (int i = 2; i <= n; ++i) {
fa[i] = (fa[i] + 19940105) % (i - 1) + 1;
}
for (int i = 1; i <= n; ++i) siz[i] = 1, tot[i] = 0;
for (int i = n; i; --i) siz[fa[i]] += siz[i];
for (int i = 1; i <= n; ++i) ++tot[siz[i]];

printf("Case #%d:\n", 10 - T);
printf("1\n");

for (auto it : d) {
int flg = solve(it);
if (flg) printf("%d\n", it);
}
printf("%d\n", n);
}
}


T2

题意:

给你一个序列,长度为 \(n\),现在需要对于任意子串,求其中 \(k\) 大和 \(k\) 小值,然后把所有的这些都加起来,对 998244353 取模,如果子串长度不够 k,则 \(k\) 大和 \(k\) 小均默认为 0。

\(n \le 10^6, 0\lt a_i \le 10^4, k \le 20\)。

\(\rm Analysis:\)

这个东西有一个很容易的暴力,定左端点,然后暴力扩展右端点,kth 可以用两个堆比较容易的维护,但是只要是对区间进行计数,那么必然绕不开区间的个数 \(n ^ 2\),无论如何无法通过本题,那你肯定是考虑每一个数是 kth 的次数,因为数个数很少,考虑如果对于每个点维护 ​​pre[i][j]​​ 表示其往前第 ​​j​​ 个小于他的数在哪里,​​nxt​​ 同理维护,那么对于这个点,其成为 k 小值等价于左右比自己小的有 \(k - 1\),那么考虑枚举左边有 ​​l​​ 个,他成为 kth 的次数就是 ​​(pre[i][l] - pre[i][l - 1]) * (nxt[i][k - l] - nxt[i][k - 1 - l])​​,也就是左端点的取值方案数乘右端点,这就是左边有 ​​l​​ 个的时候其对应的区间数,那么 k 大值同理,但是这样可能导致一个区间内多个重复的值都计算到了这个区间,解决方法是可以强制 ​​pre​​ 包含等于自己的,而 ​​nxt​​ 不包含,这样每个区间只会被最后计算。

实现上我们可以强制分配一波编号,具体而言,我们排序所有值,做两次,一次从大到小一次从小到大,具体来说,每次从大到小删除,用链表维护,这样所有剩下的都是 "小于" 自己的,于是走 k 次就好了,比维护 ​​pre nxt​​ 方便很多,而且不会重,因为第一个算了所有等于自己的,而第二个不算。

#include <algorithm>
#include <iostream>
#include <vector>

const int md = 998244353;

inline void in(int &x) {
char c = getchar();
x = 0;
while (!isdigit(c)) c = getchar();
while (isdigit(c)) x = x * 10 + c - 48, c = getchar();
}

const int maxn = 1e6;

int a[maxn + 100], pre[maxn + 100], nxt[maxn + 100], pos[maxn + 100];

signed main() {
int n, k;
in(n), in(k);
// std::vector<int> a(n + 1), pre(n + 1), nxt(n + 1), pos(n + 1);
for (int i = 1; i <= n; ++i)
in(a[i]), pos[i] = i;

std::sort(pos + 1, pos + 1 + n, [&](const int x, const int b) -> bool {
return a[x] < a[b];
});

auto calc = [&]() -> int {
int res = 0;
for (int i = 1; i <= n; ++i)
pre[i] = i - 1, nxt[i] = i + 1;

for (int i = 1; i <= n; ++i) {
int x = pos[i]; // 第 i 大/小的位置在 x

int L = x, R = x, cc = 1;
for (; cc < k; ++cc) {
if (pre[L]) L = pre[L];
else break;
}
for (; cc < k; ++cc) {
if (nxt[R] != n + 1) R = nxt[R];
else break;
}

if (cc == k) {
while (L <= x) {
if (R > n) break;
res = (res + 1ll * (L - pre[L]) * (nxt[R] - R) % md * a[x] % md) % md;
L = nxt[L], R = nxt[R];
}
}

nxt[pre[x]] = nxt[x], pre[nxt[x]] = pre[x];
}

return res;
};

int sum = calc();
std::reverse(pos + 1, pos + 1 + n);
sum += calc();
if (sum >= md) sum -= md;
printf("%d\n", sum);
}


T3

题意:

给一个 \(n\) 个点的图,问最少划分多少个块,使得任意块内包含小于 \(K\)​ 条边。

\(n \leqslant 16\)​。

\(\rm Analysis:\)

看着 \(n\) 很小,所以考虑状压,设 \(f[S]\) 表示已经分配了这一批的最少组数,那么更新就是枚举新组,考虑内部边个数即可,而内部边个数是可以预处理的。

#include <bits/stdc++.h>

signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

int n, m, k;
std::cin >> n >> m >> k;

std::vector<std::vector<int>> g(n, std::vector<int>(n));

for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v;
--u, --v;
g[u][v] = 1, g[v][u] = 1;
}

std::vector<int> c(1 << n);
for (int i = 0; i < (1 << n); ++i) {
for (int x = 0; x < n; ++x) {
if ((i >> x) & 1) {
for (int y = x + 1; y < n; ++y) {
if ((i >> y) & 1) {
c[i] += g[x][y];
}
}
}
}
}

std::vector<int> f(1 << n, 20);
f[0] = 0;
for (int s = 0; s < (1 << n); ++s) {
int t = ((1 << n) - 1) ^ s;
for (int y = t; y; y = (y - 1) & t) {
if (c[y] > k) continue;
f[s | y] = std::min(f[s | y], f[s] + 1);
}
}
std::cout << f[(1 << n) - 1] << '\n';
}


T4

题意:

给定一个网格图,有坏点,两人从左下角出发去右上角,两人除了起点终点,路径是不相交的,求路径方案数。

\(n, m \le 2000, \operatorname{md = 10^9 + 7}\).

\(\rm Analysis:\)

不相交路径组数,看着就很 LGV 吧,而 \(nm\) 很小,那么只需要大力求出方案数然后 LGV 即可。

容斥是不好的,LGV yyds!


#include <bits/stdc++.h>

const int md = 1e9 + 7;

struct modint {
int x;
modint() = default;
modint(int x) : x(x) {}
friend modint operator+(modint a, modint b) {
modint c;
c.x = a.x + b.x;
if (c.x >= md) c.x -= md;
return c;
}
friend modint operator-(modint a, modint b) {
modint c;
c.x = a.x - b.x;
if (c.x < 0) c.x += md;
return c;
}
friend modint operator*(modint a, modint b) {
modint c;
c.x = 1ll * a.x * b.x % md;
return c;
}
};

struct Mat {
int n, m;
std::vector<std::vector<modint>> a;
Mat() = default;
Mat(int n, int m) : n(n), m(m), a(n, std::vector<modint>(m)) {}
friend Mat operator*(const Mat &a, const Mat &b) {
Mat c(a.n, b.m);
for (int k = 0; k < a.m; ++k) for (int i = 0; i < c.n; ++i) for (int j = 0; j < c.m; ++j)
c.a[i][j] = c.a[i][j] + a.a[i][k] * b.a[k][j];
return c;
}
int det() {
modint ans = 1;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
while (a[j][i].x) {
int tmp = a[i][i].x / a[j][i].x;
for (int k = i; k < n; ++k)
a[i][k] = a[i][k] - a[j][k] * tmp;

std::swap(a[i], a[j]);
ans = md - ans;
}
}
ans = ans * a[i][i];
}

return ans.x;
}
inline void print() {
for (int i = 0; i < n; ++i, std::cerr << '\n') for (int j = 0; j < m; ++j)
std::cerr << a[i][j].x << ' ';
}
};

signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

int n, m;
std::cin >> n >> m;

std::vector<std::string> s(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> s[i];

std::vector<std::vector<modint>> f1(n + 1, std::vector<modint>(m + 1)), f2(n + 1, std::vector<modint>(m + 1));

f1[1][2].x = 1, f2[2][1].x = 1; // 到达起点的方案数有一种
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (s[i][j - 1] == '1') continue;
if (i != 1 && s[i - 1][j - 1] == '0') {
f1[i][j] = f1[i][j] + f1[i - 1][j];
f2[i][j] = f2[i][j] + f2[i - 1][j];
}
if (j != 1 && s[i][j - 2] == '0') {
f1[i][j] = f1[i][j] + f1[i][j - 1];
f2[i][j] = f2[i][j] + f2[i][j - 1];
}
// std::cerr << i << " " << j << " " << f1[i][j].x << " " << f2[i][j].x << '\n';
}
}

Mat res(2, 2);
res.a[0][0] = f1[n - 1][m], res.a[0][1] = f1[n][m - 1];
res.a[1][0] = f2[n - 1][m], res.a[1][1] = f2[n][m - 1];
// res.print();
std::cout << res.det() << '\n';
}


我不想就这样沦陷,迷失在黑夜,我将燃烧这生命,就算再壮烈。