NOI2022 题解合集
D1T1. P8496 [NOI2022] 众数
视 \(n, q, C_l, C_m\) 同级。
对于操作 1 和 2,直接用栈维护。
但对于操作 4,栈不支持快速合并,因此考虑双端队列启发式合并,则该部分总复杂度 \(\mathcal{O}(n\log n)\)。但直接开 \(10 ^ 6\) 个 deque 空间吃不消(之前有 \(10 ^ 5\) deque 64MB 直接 MLE 的 教训),所以考场没敢这么写。
注意到我们只需要支持尾部插入删除,快速合并,考虑双向链表,容易做到总复杂度 \(\mathcal{O}(n)\)。
关于绝对众数,有一个非常经典的方法是摩尔投票法,而摩尔投票法是可快速合并的信息,因此考虑对每个队列开一棵动态开点线段树,下标是元素的值,每个下表维护的值即该下标对应的值在队列中的出现次数,pushup 直接摩尔投票,合并时线段树合并即可。
对于操作 3,将所有相关队列进行摩尔投票,并检查最终得到的值是否出现次数严格大于一半,这个直接在维护的线段树上查一下即可。
注意点:特别注意在队列为空时双端队列头尾的处理,否则容易挂分;注意平衡 long long 和空间限制。
时空复杂度 \(\mathcal{O}(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0;
char s = getchar();
while(!isdigit(s)) s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e6 + 5;
constexpr int K = N * 21;
struct dat {
ll val, cnt; // 注意开 ll
dat operator + (const dat &x) const {
if(val == x.val) return (dat) {val, cnt + x.cnt};
if(cnt >= x.cnt) return (dat) {val, cnt - x.cnt};
return (dat) {x.val, x.cnt - cnt};
}
} val[K];
int node, R[N], ls[K], rs[K];
void modify(int l, int r, int p, int &x, int v) {
if(!x) x = ++node;
if(l == r) {
val[x].val = p;
val[x].cnt += v;
return;
}
int m = l + r >> 1;
if(p <= m) modify(l, m, p, ls[x], v);
else modify(m + 1, r, p, rs[x], v);
val[x] = val[ls[x]] + val[rs[x]];
}
int merge(int x, int y) {
if(!x || !y) return x | y;
ls[x] = merge(ls[x], ls[y]);
rs[x] = merge(rs[x], rs[y]);
return val[x] = val[x] + val[y], x;
}
vector<int> rt;
ll query(int l, int r, int p) {
if(l == r) {
ll ans = 0;
for(int it : rt) ans += val[it].cnt;
return ans;
}
int m = l + r >> 1;
if(p <= m) {
for(int &it : rt) it = ls[it];
return query(l, m, p);
}
else {
for(int &it : rt) it = rs[it];
return query(m + 1, r, p);
}
}
int cnt, hd[N], tl[N], pre[N], nxt[N], dt[N], sz[N];
void push(int x, int v) {
if(!hd[x]) hd[x] = tl[x] = ++cnt;
else nxt[tl[x]] = ++cnt, pre[cnt] = tl[x], tl[x] = cnt;
dt[cnt] = v, sz[x]++;
modify(1, N, v, R[x], 1);
}
void pop(int x) {
int v = dt[tl[x]];
if(hd[x] == tl[x]) hd[x] = tl[x] = 0;
else tl[x] = pre[tl[x]];
sz[x]--;
modify(1, N, v, R[x], -1);
}
void merge(int x, int y, int z) {
if(!hd[x]) hd[z] = hd[y], tl[z] = tl[y];
else if(!hd[y]) hd[z] = hd[x], tl[z] = tl[x];
else hd[z] = hd[x], tl[z] = tl[y], pre[hd[y]] = tl[x], nxt[tl[x]] = hd[y];
sz[z] = sz[x] + sz[y];
R[z] = merge(R[x], R[y]);
}
int n, q;
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("a.in", "r", stdin);
FILE* OUT = freopen("a.out", "w", stdout);
#endif
ios::sync_with_stdio(0);
n = read(), q = read();
for(int i = 1; i <= n; i++) {
int l = read();
while(l--) push(i, read());
}
for(int i = 1; i <= q; i++) {
int op = read();
if(op == 1) {
int x = read();
push(x, read());
}
if(op == 2) pop(read());
if(op == 3) {
int m = read();
rt.clear();
dat ans = {0, 0};
ll tot = 0;
while(m--) {
int x = read();
tot += sz[x];
ans = ans + val[R[x]];
rt.push_back(R[x]);
}
int major = -1;
if(ans.val && query(1, N, ans.val) * 2 > tot) major = ans.val;
print(major), putchar('\n');
}
if(op == 4) {
int x = read(), y = read(), z = read();
merge(x, y, z);
}
}
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/29
author: Alex_Wei
start coding at 8:28
finish debugging at 8:41
*/
D1T2. P8497 [NOI2022] 移除石子
这道题目比较抽象,比较人类智慧。
对于合法序列计数题,首先必须会判定合法序列,即考虑 \(l_i = r_i\) 的部分分。
从最基本的 \(\boldsymbol{k = 0}\) 入手 找一些性质:
-
区间长度要求 \(\geq 3\) 等价于区间长度为 \(3, 4, 5\),经典套路。
-
不会进行相同的操作二,可用若干操作一代替。
设计类插头 DP \(f_{i, j, k, l}\) 表示当前已经决策好从 \(< i\) 的位置开始的操作二,有 \(j\) 个可以延伸到 \(i\),\(k\) 个必须延伸到 \(i\),\(l\) 个必须延伸到 \(i + 1\),是否可行。转移即枚举从 \(i\) 开始的操作二个数 \(m\),可知不超过 \(3\),再枚举 \(j\) 个可以扩展到 \(i\) 的操作二中真实扩展的数量 \(p\),若 \(a_i - k - l - m - p\) 小于 \(0\) 或等于 \(1\) 则非法,否则令 \(f_{i + 1, p + k, l, m} = 1\)。检查是否存在 \(f_{n + 1, *, 0, 0} = 1\) 即可。
根据分析,\(j\) 这一维大小只需设成 \(3\),\(k \leq 3\),\(l\leq 3\) 即可覆盖至少一个可行解:
- 到 \(i - 1\) 长度已经等于 \(3\) 且还要继续延伸到 \(\geq i\) 的位置的区间只可能是 \([i - 3, i]\),\([i - 3, i + 1]\) 和 \([i - 4, i]\),因此 \(j\leq 3\)。
- 从每个位置开始的操作二数量 \(\leq 3\),因此 \(k, l\leq 3\)。
上述做法已经可以通过该档部分分。
进一步地,我们发现 \(j, k\) 这两维可以融合在一起。具体地,我们在决策 \(i\) 的时候,与其分别记录可以和必须延伸到 \(i + 1\) 的数量,不妨直接钦定有多少个真实扩展到 \(i + 1\),因为在 \(i + 1\) 处这些操作二长度均 \(\geq 3\),就等价了。
因此,设 \(f_{i, j, k}\) 表示当前决策好从 \(< i\) 的位置开始的操作二,有 \(j\) 个钦定被延伸到 \(i\),且有 \(k\) 个必须延伸到 \(i + 1\)。转移时枚举从 \(i\) 开始的操作二个数 \(l\),若 \(a_i - j - k - l\) 小于 \(0\) 或等于 \(1\) 则非法,否则钦定被延伸到 \(i + 1\) 的操作二数量 \(p\in [k, k + j]\),令 \(f_{i + 1, p, l} = 1\) 即可。检查是否存在 \(f_{n + 1, 0, 0}\) 即可。
类似分析,可知 \(j\leq 6\),\(k\leq 3\)。
进一步挖掘性质,我们发现
- 若从一个位置开始同时有长度为 \(4, 5\) 的操作二,可以简化成一次操作一和从下一个位置开始长度为 \(3, 4\) 的操作二。
因此 \(j\leq 4\),\(k\leq 2\) 仍合法。
进一步地,我们枚举跨过 \(i - 1\) 延伸到 \(i\) 且在 \(i\) 处长度 \(\geq 3\) 的操作二的所有可能情况,发现所有操作二个数 \(\geq 3\) 的情况都可以被简化成若干次操作一和不超过 \(2\) 次操作二,因此 \(j\leq 2\) 仍合法。考场上可以不断缩小 DP 某一维度并对拍从而简化状态,然后你就 win 了。
时间复杂度 \(\mathcal{O}(n)\)。代码。
考虑 \(\boldsymbol{k > 0}\)。这个「恰好」就很烦人,尝试把它弱化成「至多」,并消除弱化带来的影响。换言之,我们需要找到所有状态,使得添加小于 \(k\) 个石子有解,但添加 \(k\) 个石子后没有解。
-
若 \(k = 0\) 显然不影响。
-
若 \(k = 1\),考虑不添加任何石子的有解局面最终方案:
- 若存在至少一次操作一则必然有解,直接将石子放在操作一位置上即可。
- 若存在长度 \(> 3\) 的操作二则必然有解,将石子放在操作二左端变成一次操作一和一次操作二即可。
- 若存在长度 \(= 3\) 的操作二且 \(n > 3\) 则必然有解,将石子放在操作二两侧某空位上(\(n > 3\) 故存在),变成一次长度 \(= 4\) 的操作二即可。
综合上述分析,可知非法当且仅当 \(n = 3\) 且 \(a_1 = a_2 = a_3 = 1\),或 \(a_i = 0\)。
-
若 \(k = 2\):
- 若 \(k = 0\) 有解,将两颗石子放在任意位置用一次操作一处理,得出 \(k = 2\) 有解。
- 若 \(k = 1\) 有解,考察所有 添加一颗石子,有解变无解 的局面,即 \(n = 3\) 且 \(a_1 = a_2 = a_3 = 1\),或 \(a_i = 0\)。去掉一颗石子,要么没有石子可以去掉,即这种情况不可能发生,要么总可以重新添加两颗石子使得有解。
综合上述分析,若局面添加小于两颗石子有解,则添加两颗石子有解。
-
同理可证对于任意 \(k > 2\),若局面添加小于 \(k\) 颗石子有解,则添加 \(k\) 颗石子有解。
特判掉 \(\boldsymbol{k = 1}\) 的两种情况,若局面添加小于 \(k\) 颗石子有解,则添加 \(k\) 颗石子有解。
综上,设 \(g_{i, j, k}\) 表示为使得 \(f_{i, j, k} = 1\) 至少添加的石子数。转移类似 \(f\) 枚举 \(l\),设 \(v = a_i - j - k - l\),设 \(need\) 表示使得 \(i\) 合法至少添加的石子数,则当 \(v < 0\) 时,\(need = -v\),否则若 \(v = 1\) 则 \(need = 1\),否则 \(need = 0\)。类似 \(f\) 枚举 \(p\),则 \(g_{i + 1, p, l}\) 与 \(g_{i, j, k} + need\) 取最小值。检查是否存在 \(g_{n + 1, 0, 0} \leq k\) 即可。
时间复杂度 \(\mathcal{O}(n)\),代码。
从 \(k = 0\) 出发还有一个方向,就是 对 \(\boldsymbol{k = 0}\) 计数。
由于之前我们已经对状态进行了充足的简化,\(f_i\) 仅有 \(9\) 个状态,因此考虑 DP 套 DP,设 \(f_{i, S}\) 表示这九种状态的取值状态为 \(S\) 的方案数。显然对于较大的 \(a_i\),它们等价。具体地,由于 \(j, k, l\leq 2\),所以 \(\geq 8\) 的 \(a_i\) 是等价的。在一开始预处理出 \(tr(S, v)\) 表示从九种状态的取值状态为 \(S\) 的 \(f_i\) 开始,令 \(a_i = v\),转移到的 \(f_{i + 1}\) 的九种状态的取值状态。转移时只需枚举 \(S\) 和 \(a_i\),若 \(a_i\leq 7\) 且 \(l_i\leq a_i\leq r_i\) 则 \(f_{i + 1, tr(S, a_i)}\) 受到 \(f_{i, S}\) 的贡献,若 \(a_i = 8\) 则 \(f_{i + 1, tr(S, a_i)}\) 受到 \(f_{i, S} \times |\mathbb{Z} \cap [l_i, r_i] \cap [8, +\infty)|\) 的贡献。
通过对拍我们发现 \(\geq 6\) 的 \(a_i\) 就是等价的,感性理解是当 \(j = k = 2\) 时根本不用从 \(i\) 开始新开两个操作 \(2\),\(j + k + l = 5\) 的情况类似。这个也挺玄学的。
对于最终态 \(f_{n + 1, S}\),若 \(S\) 对应 \(j = k = 0\) 的位取值为 \(1\),则 \(f_{n + 1, S}\) 贡献至答案。时间复杂度 \(\mathcal{O}(nS)\),代码。
将对 \(k = 0\) 计数和对 \(k > 0\) 判定结合起来,因为 \(g_{i, j, k}\) 有 \(0\sim 101\) 这 \(102\) 种取值,所以乍一看状态数是 \(102 ^ 9\) 级别。但我们的感知告诉我们有很多状态都是达不到的,因为 \(g_{i, j, k}\) 在 \(j, k\) 取不同的值时,相差一定不会很大。写个爆搜发现只有 \(S = 8765\) 种状态,于是你就过了这道题目,赢麻了。
时间复杂度 \(\mathcal{O}(nS)\)。
总结:D1T2 和 D2T2 都是从简化问题入手,通过结合两个问题维度上的扩展得到正解,做这两道题让我受益匪浅。这让我想到了平行四边形,给它加上对角线相等的条件就成了矩形,给它加上邻边相等的条件就成了菱形,将两者结合在一起就成了正方形。有的时候从平行四边形到正方形是困难的,但从平行四边形到矩形和菱形是自然的,而它们的结合也是自然的。这给出了我们思考问题的一条有效的道路,同时也说明了为什么从部分分开始思考,一步一步打暴力是优秀的。我希望大家都能体会到这一点。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0, sgn = 0;
char s = getchar();
while(!isdigit(s)) sgn |= s == '-', s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return sgn ? -x : x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e3 + 5;
constexpr int S = 8765;
constexpr int mod = 1e9 + 7;
void cmin(int &x, int y) {x = x < y ? x : y;}
void add(int &x, int y) {x += y, x >= mod && (x -= mod);}
int n, k, l[N], r[N], node, f[S], g[S], tr[S][7];
map<vector<int>, int> mp;
int dfs(vector<int> cur) {
if(mp.find(cur) != mp.end()) return mp[cur];
int ind = mp[cur] = node++;
for(int a = 0; a < 7; a++) {
vector<int> suc(9, 101);
for(int j = 0; j < 3; j++)
for(int k = 0; k < 3; k++) {
int v = cur[j * 3 + k];
if(v == 101) continue;
for(int st = 0; st < 3; st++) {
int val = a - j - k - st;
int need = val < 0 ? -val : val == 1 ? 1 : 0;
for(int nj = k; nj <= min(2, j + k); nj++) cmin(suc[nj * 3 + st], v + need);
}
}
tr[ind][a] = dfs(suc);
}
return ind;
}
void solve() {
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> l[i] >> r[i];
memset(f, 0, sizeof(f)), f[0] = 1;
for(int i = 1; i <= n; i++) {
memset(g, 0, sizeof(g));
for(int j = 0; j < S; j++) {
if(!f[j]) continue;
for(int a = 0; a < 7; a++) {
int coef = 0;
if(a < 6) coef = l[i] <= a && a <= r[i];
else if(r[i] >= 6) coef = r[i] - max(6, l[i]) + 1;
if(coef == 1) add(g[tr[j][a]], f[j]);
else if(coef > 0) add(g[tr[j][a]], 1ll * f[j] * coef % mod);
}
}
swap(f, g);
}
int ans = 0;
for(auto it : mp) if(it.first[0] <= k) add(ans, f[it.second]);
if(k == 1) {
bool all0 = 1;
for(int i = 1; i <= n; i++) all0 &= !l[i];
if(all0) ans--;
if(n == 3 && l[1] <= 1 && r[1] >= 1 && l[2] <= 1 && r[2] >= 1 && l[3] <= 1 && r[3] >= 1) ans--;
}
cout << (ans % mod + mod) % mod << "\n";
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("b.in", "r", stdin);
FILE* OUT = freopen("b.out", "w", stdout);
#endif
vector<int> I(9, 101);
I[0] = 0, dfs(I);
cerr << node << endl;
int T;
cin >> T;
while(T--) solve();
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/31
author: Alex_Wei
start coding at 17:50
finish debugging at 21:17
*/
D2T1. P8499 [NOI2022] 挑战 NPC Ⅱ
题目已经明示树哈希了,我们需要一个正确的树哈希方法解决树同构问题。
设树哈希函数 \(f\),需满足若 \(T_1 \cong T_2\),则 \(f(T_1) = f(T_2)\),否则极大概率 \(f(T_1) \neq f(T_2)\)。
对于有根树 \(G, H\),设它们的根分别为 \(R_G\) 和 \(R_H\),其子节点集合分别为 \(S(R_G)\) 和 \(S(R_H)\),易得另一种描述同构的方法为 \(G\cong H\) 当且仅当
- \(|S(R_G)| = |S(R_H)|\)。
- 存在一种 \(S(R_G) \to S(R_H)\) 的 双射 \(I\),使得对于任意 \(g\in S(R_G)\),\(g\cong I(g)\)。
说人话就是根节点儿子数量相等,且存在两棵树的根儿子的一一对应关系,使得对应儿子子树同构。这样我们就将问题缩小至儿子子树的规模,这给予我们树形 DP 的思路。
具体地,设 \(f_G(i)\) 表示 \(G\) 以 \(i\) 为根的子树的哈希值,转移时考虑当前节点 \(x\) 及其所有子节点 \(y\)。回顾上述结论,我们发现它提出了这样的要求:父节点哈希值与所有子节点相关,但与子节点顺序无关。做到这一点即可满足对于任意 \(T_1\cong T_2\),\(f(T_1) = f(T_2)\)。
因此,我们需要用 具有交换律 的运算结合所有 \(f_G(y)\),以消除各个子节点之间的顺序。容易想到加法或乘法,即 \(f_G(x) = \sum f_G(y)\) 或 \(f_G(x) = \prod f_G(y)\)。但是这样冲突概率较大,我们需要进行一些改进。笔者的写法是 \(f_G(x) = (P_1 + B ^ {|son_G(x)|}\prod f_G(y))\bmod P_2\),将乘法和加法结合起来,更不容易冲突,其中 \(B, P_1, P_2\) 都是自选的一些质数。
搞定了树同构,让我们回到原问题。注意到 \(k\) 很小,所以也许爆搜 + 剪枝 + 记忆化就可以了?
设 \(f(x, y)\) 表示 \(G\) 以 \(x\) 为根的子树删去若干节点后能否同构于 \(H\) 以 \(y\) 为根的子树。看似需要做一遍二分图匹配,但注意到若 \(a\in son_G(x)\) 的子树同构于 \(b\in son_H(y)\) 的子树,我们可以直接将它们匹配掉。
证明:不妨设 \(a\) 和 \(b'\) 对应,\(a'\) 和 \(b\) 对应,若 \(a\cong b'\),则交换 \(b, b'\) 仍成立,对于 \(a'\cong b\) 同理。因此 \(a\) 删去若干节点同构于 \(b'\),\(a'\) 删去若干节点同构于 \(b\)。因为 \(a, b\) 同构,所以容易根据 \(a'\) 变成 \(b\) 和 \(a\) 变成 \(b'\) 的方案构造出 \(a'\) 变成 \(b'\) 的方案,这样 \(a\) 仍可匹配 \(b\)。换言之,在最终匹配方案中,我们总可以通过调整使得 \(a, b\) 匹配。
因此,不断删去 \(son_G(x)\) 和 \(son_H(y)\) 之间同构的元素,得到 \(I_G(x)\) 和 \(I_H(y)\)。由于这两个集合之间两两不同构,所以对于每个 \(a\in I_G(x)\),都需要在其子树内删去至少一个节点,甚至删空,可知 \(|I_G(x)| \leq sz_x - sz_y\)。考虑直接全排列枚举所有可能的匹配,递归到子问题处理。
注意递归处理前需要判一下是否存在 \(a\in I_G(x)\) 和 \(b\in I_H(y)\) 满足 \(sz_a \leq sz_b\),此时直接不合法,否则可能导致 \(sz_x - sz_y\) 变大,复杂度看起来就不正确。
加入上述剪枝后复杂度看起来很对,因为 \(k\leq 5\)。时间复杂度一个比较松的上界是 \(\mathcal{O}(nk!k)\),如果用 map 做记忆化还要多一个 \(\log\),但这个 \(\log\) 不是直接乘在 \(nk!k\) 上面,所以常数很小,或者说时间复杂度可以更紧,但已经足够了。
Upd:实际上复杂度是 \(\mathcal{O}(n2 ^ k)\),每次令 \(sz_{a_1} = C + k - 1\),\(sz_{a_2} = C + 1\),\(sz_{b_1} = sz_{b_2} = C\) 即可令 \(k\) 减去 \(1\) 但情况数乘 \(2\),卡满上界。枚举全排列换成网络流即可将匹配复杂度做到 \(\mathcal{O}(\mathrm{poly}(k))\),注意判不合法:若 \(a, b\) 匹配后不存在对于所有 \(a'\) 和 \(b'\) 均有 \(sz_{a'} > sz_{b'}\) 的匹配,则 \(f(a, b)\) 就是无用的,不能递归。
好玄学啊这题,树哈希和时间复杂度分析都很玄学。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0, sgn = 0;
char s = getchar();
while(!isdigit(s)) sgn |= s == '-', s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return sgn ? -x : x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e5 + 5;
constexpr int base = 131;
constexpr int mod = 1e9 + 7;
int C, T, k, pw[N];
vector<int> G[N], H[N];
struct solver {
vector<int> e[N];
int n, R, f[N], sz[N];
void dfs(int id) {
f[id] = pw[e[id].size()], sz[id] = 1;
for(int it : e[id]) dfs(it), f[id] = 1ll * f[id] * f[it] % mod, sz[id] += sz[it];
f[id] = (f[id] + 19260817) % mod;
sort(e[id].begin(), e[id].end(), [&](int x, int y) {return f[x] < f[y];});
}
void init() {
n = read();
for(int i = 1; i <= n; i++) e[i].clear();
for(int i = 1; i <= n; i++) {
int ff = read();
if(ff != -1) e[ff].push_back(i);
else R = i;
}
dfs(R);
}
} g, h;
map<int, int> f[N];
bool dfs(int x, int y) {
if(g.e[x].size() < h.e[y].size()) return 0;
if(g.sz[x] == h.sz[y]) return g.f[x] == h.f[y];
if(f[x].find(y) != f[x].end()) return f[x][y];
vector<int> gson, hson;
int pg = 0, ph = 0;
auto grt = [&]() {gson.push_back(g.e[x][pg++]);};
auto hrt = [&]() {hson.push_back(h.e[y][ph++]);};
while(pg < g.e[x].size() && ph < h.e[y].size()) {
if(g.f[g.e[x][pg]] == h.f[h.e[y][ph]]) pg++, ph++;
else if(g.f[g.e[x][pg]] < h.f[h.e[y][ph]]) grt();
else hrt();
}
while(pg < g.e[x].size()) grt();
while(ph < h.e[y].size()) hrt();
int cut = g.sz[x] - h.sz[y];
if(gson.size() > cut) return 0;
vector<int> p(gson.size());
for(int i = 0; i < gson.size(); i++) p[i] = i;
do {
bool ok = 1;
for(int i = 0; i < hson.size(); i++) ok &= g.sz[gson[p[i]]] > h.sz[hson[i]];
if(!ok) continue;
for(int i = 0; i < hson.size(); i++) ok &= dfs(gson[p[i]], hson[i]);
if(ok) return f[x][y] = 1;
} while(next_permutation(p.begin(), p.end()));
return f[x][y] = 0;
}
void solve() {
g.init(), h.init();
for(int i = 1; i <= g.n; i++) f[i].clear();
puts(dfs(g.R, h.R) ? "Yes" : "No");
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("d.in", "r", stdin);
FILE* OUT = freopen("d.out", "w", stdout);
#endif
for(int i = pw[0] = 1; i < N; i++) pw[i] = 1ll * pw[i - 1] * base % mod;
C = read(), T = read(), k = read();
while(T--) solve();
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/29
author: Alex_Wei
start coding at 8:55
finish debugging at 9:14
*/
D2T2. P8500 [NOI2022] 冒泡排序
首先对题目进行初步观察:
- 冒泡排序交换次数就是逆序对数,只能说这个皮套得很离谱。
- \(V_i\) 可以离散化,因为在最终方案中若存在 \(a_i\) 不等于任何 \(V_j\),则将其调整到大于它的最小的 \(V_j\) 或小于它的最大的 \(V_j\) 均不使得逆序对数量变多。
受到 P4229 某位歌姬的故事 的影响,我在考场上以为这题是神仙 DP 题,不会做,摆烂了。但仔细想想就会发现 P4229 只要考虑最后一个满足条件的位置,但是本题逆序对数需要考虑当前位置和之前所有位置产生的联合贡献,看起来不太能做。
从部分分入手,这题部分分设置得真好。
考虑 特殊性质 B。除去无解的情况,一些位置的值已经固定,其余位置可以任意取值。
性质 1:任意取值的位置必然不产生逆序对。
证明:若 \(i < j\) 且 \(a_i > a_j\),交换 \(a_i, a_j\) 显然不劣。
据此,从后往前枚举所有任意取值的位置 \(i\),每次选择使得与固定取值位置之间贡献最少的值作为 \(a_i\)。设 \(c_v\) 表示当前位置选择 \(v\) 产生的贡献,则 \(a_i = \mathrm{argmin}_{v = 1} ^ m c_v\)。这样决策 \(a_i\),可证任意取值位置之间不产生逆序对:从后往前扫的过程中,若遇到固定取值位置 \(j\),则我们的操作是将 \(c\) \([1, a_j - 1]\) 区间减 \(1\),\([a_j + 1, m]\) 区间加 \(1\),因此若 \(i < j\) 且 \(c_i \leq c_j\),那么接下来任意时刻均有 \(c_i\leq c_j\),因此具有 决策单调性。
注意尽管具有决策单调性,但 \(c\) 不具有凸性,不可以直接用指针维护。用线段树区间加全局 \(\mathrm{argmin}\) 维护 \(c\) 即可 \(\mathcal{O}(n\log n)\) 性质 B。代码。
进一步地,考虑 特殊性质 C。限制相对独立,我们找一些贪心思想。逆序对数相关,我们希望 较小的数尽可能排在前面。这就启发我们得到如下结论:对于限制 \((l, r, V)\),将 \(V\) 尽可能往前放,即必然有 \(a_l = V\)。因为观察 \(a_l \sim a_r\),如果 \(a_l > V\),那么必然存在 \(p\in (l, r]\) 满足 \(a_p = V\),\(a_l\) 与 \(a_p\) 之间形成逆序对,交换更优。这样一来,恰好等于 的要求就被搞定了,\((l, r, V)\) 对 \(a_{l + 1}\sim a_r\) 的限制仅仅是不小于 \(V\)。
借鉴性质 B 的思路,我们有如下算法:设 \(b_i\) 表示 \(a_i\) 被限制 \(\geq b_i\)。对于所有限制 \((l, r, V)\),将 \(c_1 \sim c_{V - 1}\) 区间加上 \(r - l + 1\),表示如果选择小于 \(V\) 的数,则会和 \(a_l \sim a_r\) 形成 \(r - l + 1\) 个逆序对,并令 \(b_i\gets V(i\in [l, r])\)。仍然从后往前考虑所有位置,对于位置 \(i\),若其被限制 \(\geq b_i\),则将 \(c_1\sim c_{b_i - 1}\) 区间减 \(1\),这和性质 B 一样。接下来,若 \(a_i\) 还没有被固定取值,即 \(i\) 不是某限制的 \(l\),则令 \(a_i = \mathrm{argmin}_{v = b_i} ^ m c_v\)。注意这里不是令 \(a_i\) 为全局 \(\mathrm{argmin}\) 再和 \(b_i\) 取 \(\max\),因为 \(c\) 没有凸性,我在考场上这么写过不了样例 5,以为是结论假了,寄。最后令 \(c_{a_i + 1} \sim c_r\) 区间加 \(1\),意义显然。时间复杂度 \(\mathcal{O}(n\log n)\)。代码。
这个做法正确性证明有点难,大家根据性质 B 做法感性理解即可。
接下来考虑 特殊性质 A。若 \(V = 1\),显然 \(a_l\sim a_r\) 均为 \(1\),否则将所有 \(V = 0\) 的限制拎出来,仍然是从后往前考虑所有位置。因为我们希望较小的数尽可能排在前面,所以我们 当且仅当再不放 \(0\) 就会出现无解的时候,我们才选择固定当前位为 \(0\),这其实和性质 C 的贪心有异曲同工之妙。贪心过程容易维护:
- 进行处理使得区间不相互包含(但注意若 \(I_1 \subseteq I_2\),那么是 \(I_1\) 限制更紧,要保留 \(I_1\) 而非 \(I_2\)),然后用指针维护最后一个未满足的区间,扫一遍所有未被填入 \(1\) 的位置。
- 或者将所有区间按照左端点从大到小排序后考虑,用 set 维护已经选中和未被选中的位置。若不存在已经选中的位置属于 \(I\) 则在未被选中的位置集合中查询 \(l\) 的后继,若不存在或大于 \(r\) 则无解,否则选中该后继。
固定一些位置等于 \(0\) 之后做一遍特殊性质 B 即可。时间复杂度 \(\mathcal{O}(n\log n)\)。
贪心正确性证明:考虑 \(V = 0\) 的限制区间集 \(S\),考虑左端点最大的任意限制 \(I\in S\),我们在 \(l_I\) 处固定 \(a_{l_I} = 0\)。因为不存在左端点比 \(I\) 更靠右的区间,再根据 \(I\) 的限制,在 \(\geq l_I\) 的位置必须至少一个位置固定为 \(0\),而选择 \(l_I\) 可以让尽可能多的其它区间被满足,选择其它位置通过调整总是不优于选择 \(l_I\),证毕。
正解考虑结合特殊性质 A 和特殊性质 C 的做法,对每个值 \(v\),将所有 \(b_i = v\) 的位置拎出来(\(b_i > v\) 的位置不能放 \(v\)),所有 \(V = v\) 的限制区间拎出来,对这些位置和区间做性质 A,最终得到一些已经固定的 \(a\),结合 \(b_i\) 做性质 C 即可。时间复杂度 \(\mathcal{O}(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0, sgn = 0;
char s = getchar();
while(!isdigit(s)) sgn |= s == '-', s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return sgn ? -x : x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e6 + 5;
int n, m, d[N], fa[N], val[N], low[N];
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}
struct BIT {
int c[N];
void clear() {for(int i = 1; i <= m; i++) c[i] = 0;}
void add(int x, int v) {while(x <= m) c[x] += v, x += x & -x;}
int query(int x) {int s = 0; while(x) s += c[x], x -= x & -x; return s;}
int query(int l, int r) {return query(r) - query(l - 1);}
} tr;
struct limit {
int l, r, v;
} c[N];
set<int> pos[N];
vector<limit> buc[N];
struct Segtree1 {
struct dat {
int mn, pos;
dat operator + (const dat &x) const { // ensure that pos < x.pos
assert(pos < x.pos);
return mn <= x.mn ? *this : x;
}
} val[N << 2];
int laz[N << 2];
void build(int l, int r, int x) {
laz[x] = 0;
if(l == r) return val[x] = {0, l}, void();
int m = l + r >> 1;
build(l, m, x << 1), build(m + 1, r, x << 1 | 1);
val[x] = val[x << 1] + val[x << 1 | 1];
}
void tag(int x, int v) {val[x].mn += v, laz[x] += v;}
void down(int x) {
if(laz[x]) {
tag(x << 1, laz[x]);
tag(x << 1 | 1, laz[x]);
laz[x] = 0;
}
}
void modify(int l, int r, int ql, int qr, int x, int v) {
if(ql > qr) return;
if(ql <= l && r <= qr) return tag(x, v);
int m = l + r >> 1;
down(x);
if(ql <= m) modify(l, m, ql, qr, x << 1, v);
if(m < qr) modify(m + 1, r, ql, qr, x << 1 | 1, v);
val[x] = val[x << 1] + val[x << 1 | 1];
}
dat query(int l, int r, int ql, int qr, int x) {
if(ql <= l && r <= qr) return val[x];
int m = l + r >> 1;
dat ans = {N, 0};
if(ql <= m) ans = ans + query(l, m, ql, qr, x << 1);
if(m < qr) ans = ans + query(m + 1, r, ql, qr, x << 1 | 1);
return ans;
}
} sgt1;
void solve() {
n = read(), m = read();
for(int i = 1; i <= n + 1; i++) fa[i] = i, val[i] = low[i] = 0;
for(int i = 1; i <= m; i++) {
c[i].l = read(), c[i].r = read();
d[i] = c[i].v = read();
}
sort(d + 1, d + m + 1);
for(int i = 1; i <= m; i++) c[i].v = lower_bound(d + 1, d + m + 1, c[i].v) - d;
for(int i = 1; i <= m; i++) buc[i].clear(), pos[i].clear();
for(int i = 1; i <= m; i++) buc[c[i].v].push_back(c[i]);
for(int i = m; i; i--)
for(auto it : buc[i]) {
while(1) {
int p = find(it.l);
if(p > it.r) break;
fa[p] = p + 1, low[p] = i, pos[i].insert(p);
}
}
for(int i = 1; i <= m; i++) {
sort(buc[i].begin(), buc[i].end(), [&](limit x, limit y) {return x.l > y.l;});
set<int> settle;
for(auto it : buc[i]) {
auto pt = settle.lower_bound(it.l);
if(pt != settle.end() && *pt <= it.r) continue;
pt = pos[i].lower_bound(it.l);
if(pt == pos[i].end() || *pt > it.r) return puts("-1"), void();
settle.insert(*pt);
val[*pt] = i;
pos[i].erase(pt);
}
}
sgt1.build(1, m, 1);
for(int i = 1; i <= n; i++) if(low[i]) sgt1.modify(1, m, 1, low[i] - 1, 1, 1);
for(int i = n; i; i--) {
if(low[i]) sgt1.modify(1, m, 1, low[i] - 1, 1, -1);
if(!val[i]) val[i] = sgt1.query(1, m, low[i], m, 1).pos;
sgt1.modify(1, m, val[i] + 1, m, 1, 1);
}
ll ans = 0;
tr.clear();
for(int i = 1; i <= n; i++) ans += tr.query(val[i] + 1, m), tr.add(val[i], 1);
cout << ans << "\n";
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("bubble6.in", "r", stdin);
FILE* OUT = freopen("e.out", "w", stdout);
#endif
int T;
cin >> T;
while(T--) solve();
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/29
author: Alex_Wei
start coding at 16:34
finish debugging at 20:21
*/