AHOI 2022
vp 成绩:\(100 + 100 + 60 + 45\),呜呜,我好菜。
A. 排列
考虑把排列看成置换,有边 \(i \rightarrow p_i\), \(p_i^{k}\) 事实上往后走 \(k\) 的编号。考虑每个环独立,那每个环限制就是 \(k\) 得是环大小的倍数,那 \(v\) 自然是 LCM 了。
考虑 \(f(i, j) = 0\) 就是他们在一个环里,只有不在一个环里有贡献,这个 swap 相当于是把这两个环合起来,变化的 v 只跟选择的俩环大小有关系,跟具体编号没关系。
一个经典结论是,\(\sum a_i = n\) 的形式,值不同的 \(a_i\) 只有 \(\sqrt{n}\) 量级。 那环本质不同只有根号种,那就可以暴力枚举两个环大小是啥,这样消耗也是 \(O(n)\) 的,然后我们相当于要支持维护一个集合的 LCM,支持删除两个数,加入一个数,新的 LCM。考虑质因数分解每个单独考虑后相当于加入删除,维护 max。因为质因数已经有个 log 了,为了规避掉 log,我们发现最多删两个数,那么只要预先处理最大的三个就好了,新的 max 肯定在这三个后后添加中产生。
\(O(n \log n)\)
// Skyqwq
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pb push_back
#define mp make_pair
typedef long long LL;
typedef pair<int, int> PII;
template <typename T> void inline read(T &x) {
x = 0; char s = getchar(); int f = 1;
while (s < '0' || s > '9') { if (s == '-') { f = -1; } s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
template <typename T> bool inline chkMax(T &x, T y) { return y > x ? x = y, 1 : 0; }
template <typename T> bool inline chkMin(T &x, T y) { return y < x ? x = y, 1 : 0; }
const int P = 1e9 + 7, N = 5e5 + 5;
int n, a[N], f[N], sz[N], c[N], d[N], t, inv[N];
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
void inline merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return;
if (sz[x] > sz[y]) swap(x, y);
f[x] = y, sz[y] += sz[x];
}
vector<int> g[N];
vector<PII> e[N];
bool st[N];
int pr[N], tot, p0[N];
vector<PII> fc[N];
void inline clr() {
t = 0;
for (int i = 1; i <= n; i++) c[i] = d[i] = 0, g[i].clear();
}
int now, cnt[N];
int inline get(int x) {
int mx = 1;
for (int v: g[x]) cnt[v]++;
for (PII v: e[x]) cnt[v.fi] += v.se;
for (int v: g[x])
if (cnt[v]) chkMax(mx, v), cnt[v] = 0;
for (PII v: e[x])
if (cnt[v.fi]) chkMax(mx, v.fi), cnt[v.fi] = 0;
return mx;
}
void inline chg(int x, int d) {
for (PII o: fc[x]) {
now = 1ll * now * inv[get(o.fi)] % P;
e[o.fi].pb(mp(o.se, d));
now = 1ll * now * get(o.fi) % P;
}
}
void inline del(int x) {
for (PII o: fc[x])
e[o.fi].clear();
}
void inline add(int &x, int y) {
x += y;
if (x >= P) x -= P;
}
void inline work() {
now = 1;
for (int i = 1; i <= n; i++) {
if (g[i].size()) {
sort(g[i].begin(), g[i].end(), greater<int>() );
while (g[i].size() > 3) g[i].pop_back();
now = 1ll * now * g[i][0] % P;
}
}
int ans = 0;
for (int i = 1; i <= t; i++) {
int u = d[i];
if (c[u] > 1) {
int la = now;
chg(u + u, 1);
chg(u, -1);
chg(u, -1);
del(u + u); del(u);
add(ans, 1ll * now * c[u] % P * (c[u] - 1) % P * u % P * u % P);
now = la;
}
for (int j = i + 1; j <= t; j++) {
int v = d[j];
int la = now;
chg(u + v, 1), chg(u, -1), chg(v, -1);
add(ans, 2ll * now * c[u] % P * c[v] % P * u % P * v % P);
del(u + v), del(u), del(v);
now = la;
}
}
printf("%d\n", ans);
}
void inline apd(int x) {
for (PII o: fc[x])
g[o.fi].pb(o.se);
}
void inline prw(int n) {
p0[1] = 1;
inv[1] = 1;
for (int i = 2; i <= n; i++) {
inv[i] = ((LL)P - P / i) * inv[P % i] % P;
}
for (int i = 2; i <= n; i++) {
if (!st[i]) pr[++tot] = i, p0[i] = i;
for (int j = 1; pr[j] * i <= n; j++) {
st[i * pr[j]] = 1;
p0[i * pr[j]] = pr[j];
if (i % pr[j] == 0) break;
}
}
for (int i = 1; i <= n; i++) {
int x = i;
while (x != 1) {
int p = p0[x], v = 1;
while (x % p == 0) v *= p, x /= p;
fc[i].pb(mp(p, v));
}
}
}
int main() {
//freopen("perm.in", "r", stdin);
//freopen("perm.out", "w", stdout);
prw(5e5);
int T; read(T);
while (T--) {
read(n);
for (int i = 1; i <= n; i++) read(a[i]), f[i] = i, sz[i] = 1;
for (int i = 1; i <= n; i++) merge(i, a[i]);
for (int i = 1; i <= n; i++) {
if (find(i) == i) {
c[sz[i]]++;
apd(sz[i]);
}
}
for (int i = 1; i <= n; i++)
if (c[i]) d[++t] = i;
work();
clr();
}
}
B. 钥匙
这里称 \(1\) 是钥匙,\(2\) 是宝箱。
考虑必须用到最多 \(5\) 个 \(1\) 这个信息,一个大概框架是,你可以预先对于每个 \(2\),枚举所有可能的 \(1\),然后考虑何种路径可以被这对贡献到。
考虑对于每个颜色单独考虑,假设一次经过的长这样 \(11222112\)。考虑把 \(+1\) 的贡献记在 \((1, 4)\) (表示第一位和第四位),\((2, 3),(7, 8)\),这样,这个过程可以把 \(1\) 看成左括号,\(2\) 看成右括号,做括号匹配的过程。
那这样考虑枚举这样的 \(1, 2\) 点对,他能产生贡献的充要条件是:
- 它在询问路径上
- 中间部分是正好括号匹配的:(将 \(1\) 看做 \(1\),\(2\) 看做 \(-1\),前缀和 \(\ge 0\),并且和 \(= 0\))
这样设计的贡献是好的,因为已经让互相匹配的尽可能进,而且不重不漏。
在路径上的限制可以通过是否是祖先形式分讨变为询问 \(s, e\) 要分别在 \(dfn\) 的一个区间这种形式,就是所有矩形加,最后单点查,那么离线差分下来树状数组就好了。
枚举 \(1, 2\) 点对的过程比较好的实现方式是,建虚树,然后 dfs?(我能想到的?
\(O(5 n \log n + m\log n)\)
// Skyqwq
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pb push_back
#define mp make_pair
typedef long long LL;
typedef pair<int, int> PII;
template <typename T> void inline read(T &x) {
x = 0; char s = getchar(); int f = 1;
while (s < '0' || s > '9') { if (s == '-') { f = -1; } s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
template <typename T> bool inline chkMax(T &x, T y) { return y > x ? x = y, 1 : 0; }
template <typename T> bool inline chkMin(T &x, T y) { return y < x ? x = y, 1 : 0; }
const int N = 5e5 + 5, M = 1e6 + 5;
int n, m, T[N], C[N], tp[N], dfn[N], dfncnt, sz[N], fa[N], son[N], d[N], pre[N];
vector<int> g[N];
void dfs1(int u) {
sz[u] = 1;
for (int v: g[u]) {
if (v == fa[u]) continue;
fa[v] = u;
d[v] = d[u] + 1;
dfs1(v);
sz[u] += sz[v];
if (sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int top) {
tp[u] = top, dfn[u] = ++dfncnt;
pre[dfn[u]] = u;
if (son[u]) dfs2(son[u], top);
for (int v: g[u]) {
if (v == fa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
int inline lca(int x, int y) {
while (tp[x] != tp[y]) {
if (d[tp[x]] < d[tp[y]]) swap(x, y);
x = fa[tp[x]];
}
return d[x] < d[y] ? x : y;
}
vector<int> b[N];
int inline cmp(int x, int y) {
return dfn[x] < dfn[y];
}
int s[N], top, ans[M];
vector<int> e[N];
void inline addE(int x, int y) {
//cout << x << " " << y << " bd?\n";
e[x].pb(y), e[y].pb(x);
}
vector<int> z;
void inline ins(int x) {
z.pb(x);
if (!top) { s[++top] = x; return; }
int p = lca(s[top], x);
while (top > 1 && d[s[top - 1]] >= d[p]) {
addE(s[top - 1], s[top]);
top--;
}
if (s[top] != p) {
addE(s[top], p);
s[top] = p;
z.pb(p);
}
s[++top] = x;
}
int S, nc;
// x is y ancestor?
int inline isA(int x, int y) {
return dfn[x] <= dfn[y] && dfn[y] < dfn[x] + sz[x];
}
// x -> y subpath cont.
int jp(int x, int y) {
int la = 0;
while (tp[y] != tp[x])
la = tp[x], x = fa[tp[x]];
if (x == y) return la;
return pre[dfn[y] + 1];
}
// dfn[s] in [A, B], dfn[e] in [C, D]: +1
vector<PII> t[N];
void inline join(int A, int B, int C, int D) {
if (A > B || C > D) return;
t[A].pb(mp(C, 1));
t[A].pb(mp(D + 1, -1));
t[B + 1].pb(mp(C, -1));
t[B + 1].pb(mp(D + 1, 1));
}
void inline insert(int x, int y) {
if (isA(x, y)) {
int z = jp(y, x);
join(1, dfn[z] - 1, dfn[y], dfn[y] + sz[y] - 1);
join(dfn[z] + sz[z], n, dfn[y], dfn[y] + sz[y] - 1);
} else if (isA(y, x)) {
int z = jp(x, y);
join(dfn[x], dfn[x] + sz[x] - 1, 1, dfn[z] - 1);
join(dfn[x], dfn[x] + sz[x] - 1, dfn[z] + sz[z], n);
} else {
join(dfn[x], dfn[x] + sz[x] - 1, dfn[y], dfn[y] + sz[y] - 1);
}
}
void dfs(int u, int la, int w) {
if (w < 0) return;
for (int v: e[u]) {
if (v == la) continue;
if (!w && C[v] == nc && T[v] == 2) {
insert(S, v);
continue ;
}
int nv = 0;
if (C[v] == nc) {
if (T[v] == 2) nv--;
else nv++;
}
dfs(v, u, w + nv);
}
}
vector<PII> a[N];
int c[N];
void inline add(int x, int y) {
for (; x <= n; x += x & -x) c[x] += y;
}
int inline ask(int x) {
int ret = 0;
for (; x; x -= x & -x) ret += c[x];
return ret;
}
int main() {
// freopen("keys.in", "r", stdin);
// freopen("keys.out", "w", stdout);
read(n), read(m);
for (int i = 1; i <= n; i++) read(T[i]), read(C[i]), b[C[i]].pb(i);
for (int i = 1, u, v; i < n; i++) {
read(u), read(v);
g[u].pb(v), g[v].pb(u);
}
dfs1(1);
dfs2(1, 1);
for (int i = 1; i <= n; i++) {
if (!b[i].size()) continue;
nc = i;
sort(b[i].begin(), b[i].end(), cmp);
for (int v: b[i]) ins(v);
while (top > 1) addE(s[top - 1], s[top]), --top;
for (int v: b[i])
if (C[v] == i && T[v] == 1)
S = v, dfs(v, 0, 0);
for (int v: z) e[v].clear();
z.clear();
top = 0;
}
for (int i = 1; i <= m; i++) {
int u, v; read(u), read(v);
a[dfn[u]].pb(mp(dfn[v], i));
}
for (int i = 1; i <= n; i++) {
for (PII o: t[i])
add(o.fi, o.se);
for (PII o: a[i])
ans[o.se] = ask(o.fi);
}
for (int i = 1; i <= m; i++) printf("%d\n", ans[i]);
}
C. 山河重整
首先这个问题肯定需要转化找到一个更好的充要条件。
- 对于任意 \(i\),满足 \(S\) 中 \(\le i\) 的和要 \(\ge i\)
证明:这个肯定的必要的,考虑充分怎么证。1, 2 肯定是必需在里面,刚好满足这个条件的,考虑归纳 \(i \ge 3\),假设前 \(i - 1\) 个和都能用 \(\le x\) 表达出来,考虑找到一个最小的 \(x\) 使得 \(\le x\) 的数加起来 \(\ge i\) ,设这个和是 \(t\),那么必然有 \(i \le t < 2i\),那么 \(t - i < i\) 也能被表示了,用总的减去 \(t - i\) 就得到 \(i\) 了。
那么就有一个 \(O(n^2)\) 的 dp,就按 \(1\) 到 \(n\) 顺序加入,记一下当前和(可以对 \(n\) 取 min),任意时刻都得满足限制。
考虑优化这个事情,其实是困难的。(后续全部拟合 ei,呜呜,我好菜。
合法的抽象特征似乎有点难提取,考虑不合法有一个共同的特征,考虑最小的 \(x\),使得 \(\le x\) 的和 \(< x\),那么 \(\le x - 1\) 的和必然是 \(x - 1\)!
那么我们可以从新的视角设计状态是 \(f_i\) 表示 \(\le i\) 的一个子集的和恰好是 \(i\),并且任意 \(x \le i\) 都合法(小于等于的和大于自己)。
\(f\) 的计算可以先通过计算 \(f\) 的整数拆分,减去不合法情况。不合法情况是什么呢,其实就是刚才提到全局不合法的一个局部,即这个拆分存在一个 \(x\) 使得 \(\le x\) 的和是 \(x\) 并且之前都合法,那么就是 \(f_x\),然后没有 \(x+1\),加上 \(x + 2 \sim i\) 部分的一个和恰好是 \(i\) 。
考虑整数拆分的计算,比如现在计算 \(n\) 的分拆,一个发现就是这个集合个数是根号量级的(((咋这么喜欢考这个,那么计数是可以转化角度,一个好的理解是考虑这种矩形图(类似 Ferrers 或者杨表
每列代表一个数,竖着长度代表数值。上图是一个 \(6+5+3+1 = 15\) 的例子。
那么这个矩阵有着最多根号量级的列。之前自然的 dp 都是一列一列加,不妨考虑一行一行加。
假设有 \(p\) 个数,那么一个方案可以表示成 \(n = \sum v_p \times p\) 的形式,期中 \(v_p \ge 1\),而 \(p\) 是根号量级。(这是一个经典映射?
所以可以看成有根号个数的完全背包之类的形式,完成 \(O(n \sqrt {n})\) 分拆的计算。
考虑前面的 \(f\) 对后面 \(f\) 的那个贡献,假设 \(j\) 对 \(i\) 的贡献,那么肯定有 \(j + j + 2 \le i\),其实可以类似的做,我们可以从大到小枚举 \(p\),那么这 \(p\) 个数都有 \(\ge j + 2\),每次先叠行,然后加入 \(f_j\) 后面放 \(p\) 个这样的贡献,这样就是 \(O(n \sqrt{n})\) 。但这要求是前面的 \(f\) 都算好了。
考虑到 \(j \le \frac{i}{2}\),那么我们可以考虑递归的形式,先算好 \(n / 2\) 之前的部分,然后花费 \(O(n \sqrt {n})\) 的代价算之后的部分,这个复杂度仍然是不超过 \(O(n \sqrt {n})\)(类似于倍增 FFT 的过程。
// Skyqwq
#include <bits/stdc++.h>
#define pb push_back
#define fi first
#define se second
#define mp make_pair
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N = 5e5 + 5;
int n, P, f[N], g[N], pw[N];
void inline add(int &x, int y) {
x += y;
if (x >= P) x -= P;
}
void inline del(int &x, int y) {
x -= y;
if (x < 0) x += P;
}
void inline work(int m) {
if (m <= 1) return;
work(m / 2);
for (int i = 0; i <= m; i++) g[i] = 0;
for (int i = m; i; i--) {
if (i * (i + 1ll) / 2 > m) continue;
for (int j = m; j >= i; j--) g[j] = g[j - i];
for (int j = 0; j + (j + 2) * i <= m; j++)
add(g[j + (j + 2) * i], f[j]);
for (int j = i; j <= m; j ++) add(g[j], g[j - i]);
}
for (int i = m / 2 + 1; i <= m; i++) del(f[i], g[i]);
}
int main() {
read(n), read(P);
for (int i = n; i; i--) {
if (i * (i + 1ll) / 2 > n) continue;
for (int j = n; j >= i; j--)
f[j] = f[j - i];
add(f[i], 1);
for (int j = i; j <= n; j ++) add(f[j], f[j - i]);
}
f[0] = 1;
work(n);
pw[0] = 1;
for (int i = 1; i <= n; i++) pw[i] = pw[i - 1] * 2 % P;
int ans = pw[n];
for (int i = 0; i < n; i++) {
del(ans, 1ll * f[i] * pw[n - i - 1] % P);
}
printf("%d\n", ans);
return 0;
}
D. 回忆
生活在题面里的他们,是一群怪异的少年。
对城市中修建道路需满足的基本物理限制熟视无睹,沉迷于十万个城市、百万条道路上的各种结构。
明明知道真正需要的数字庞大到无法计算,却偏要关心它模一个奇怪素数之后得到的结果。
如此智力超群的他们,却总是在自己提出的诡异的问题下败下阵来,把它们一股脑地丢给你们来做。
如今,他们长大了。他们学习到更普适的理论,习惯了更抽象的符号,不必再思考如此古怪的问题。但他们不曾料到,你们却以这些 “无用” 的问题为驱动,于计算机学科体系的一隅,开垦出了一片独属于 OI 的新天地。
有一天,他们各自回忆起了少年时期提出的问题。
这个大致思路在 vp 的时候考量过,在写 B 子任务时,但写了一下,写挂了,对于免费提供的(从上面,很没理解,那实际上你可以看做换根,ans = 所有的 - 两两匹配上的接口。
然后万万没想到,去掉包含关系后有这么好的性质。
- \(t\) 两两不同。这我都不知道?????????????????
然后考虑这个做法的细节。
-
如果能平分拼起来,你贡献首先是加入 sum,然后 (sum + 免费)/ 2 拼起来的减省。
-
首先你需要维护这个 \(z\),考虑这个 \(z\) 在过程中怎么变。你会走一条链,每次去掉 \(s = p\) 这个点的某些研究。那么你考虑,你相当于是给了一条免费路径,那你之后的拼接不会考虑他拼起来,那相当于是查到这个子树时,会 \(-1\)!!!!!!!!!!
-
维护的特殊标记点两两不交。
-
从这个点跳到重儿子时,如果重儿子是特殊标记点,清除就好了。
还是很没理解,我该怎么办??
// Skyqwq
#include <bits/stdc++.h>
#define pb push_back
#define fi first
#define se second
#define mp make_pair
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N = 2e5 + 5;
int n, m, dfn[N], fa[N], dfncnt, L[N], R[N], sz[N];
vector<int> g[N], w[N];
struct BIT{
int c[N];
void inline add(int x, int k) {
for (; x <= n; x += x & -x) c[x] += k;
}
int inline ask(int x) {
int ret = 0;
for (; x; x -= x & -x) ret += c[x];
return ret;
}
void inline clr() {
for (int i = 1; i <= n; i++) c[i] = 0;
}
} t1, t2;
struct E{
int s, t;
} e[N];
bool vis[N];
int fr[N];
void inline clr() {
dfncnt = 0;
t1.clr(), t2.clr();
for (int i = 1; i <= n; i++) vis[i] = fr[i] = 0, g[i].clear(), w[i].clear(), fa[i] = sz[i] = dfn[i] = 0;
}
void dfs1(int u) {
dfn[u] = ++dfncnt;
sz[u] = 1;
L[u] = dfn[u];
for (int v: g[u]) {
if (v == fa[u]) continue;
fa[v] = u;
dfs1(v);
sz[u] += sz[v];
}
R[u] = dfncnt;
}
bool inline cmp(E x, E y) {
if (dfn[x.s] != dfn[y.s]) return dfn[x.s] < dfn[y.s];
return dfn[x.t] > dfn[y.t];
}
bool dfs2(int u) {
bool o = 0;
for (int v: g[u]) {
if (v == fa[u]) continue;
o |= dfs2(v);
}
if (!o && vis[u]) t1.add(dfn[u], 1);
return o | vis[u];
}
void solve() {
int p = 1, re = 0, ans = 0;
while (p) {
int sum = 0, mx = 0, big = 0;
for (int v: g[p]) {
if (v != fa[p]) {
int w = t1.ask(R[v]) - t1.ask(L[v] - 1);
if (chkMax(mx, w)) big = v;
sum += w;
}
}
int rt = sum - mx;
int rs = rt + re;
if (mx <= rs) {
ans += sum - (sum + re) / 2;
break;
}
ans += rt;
re += rt;
for (int v: w[p]) {
if (dfn[v] < L[big] || dfn[v] > R[big]) continue;
int z = t2.ask(dfn[v]);
if (z) {
t1.add(dfn[z], 1);
t2.add(L[z], -z), t2.add(R[z] + 1, z);
fr[z]--;
} else if (re) {
re--;
} else {
ans++;
}
t1.add(dfn[v], -1);
t2.add(L[v], v), t2.add(R[v] + 1, -v);
fr[v]++;
}
if (fr[big]) t2.add(L[big], -big), t2.add(R[big] + 1, big);
p = big;
re += fr[big], fr[big] = 0;
}
printf("%d\n", ans);
}
void inline work() {
read(n), read(m);
for (int i = 1, u, v; i < n; i++)
read(u), read(v), g[u].pb(v), g[v].pb(u);
for (int i = 1; i <= m; i++) read(e[i].s), read(e[i].t);
dfs1(1);
// 清除包含关系
sort(e + 1, e + 1 + m, cmp);
for (int i = 1; i <= m; i++) {
int cnt = t1.ask(R[e[i].t]) - t1.ask(L[e[i].t] - 1);
if (!cnt) {
w[e[i].s].pb(e[i].t);
vis[e[i].t] = 1;
}
t1.add(dfn[e[i].t], 1);
}
t1.clr();
// 维护 t1,维护在最底下、未解决的 t (s 在当前子树里)
dfs2(1);
// 维护 t2,维护那些互不成祖先关系的特殊点,覆盖他们的子树,帮助快速查
solve();
clr();
}
int main() {
// freopen("memory6.in", "r", stdin);
// freopen("m.out", "w", stdout);
int Case; read(Case);
while (Case--) work();
return 0;
}