D
https://atcoder.jp/contests/arc130/tasks/arc130_d
给一个树,对节点赋值1~n
,两两互不相同,
限制:树上的点要么小于它直接相邻所有点的值,要么大于它直接相邻所有点的值
求方案数.
题解
显然任意选一个为根, 大于和方案和小于的方案对称性,可以只算一个然后乘2
也可以,看成二分图,但是是求满足拓扑序的赋值方案数
注意到大于和小于关系,只需要颠倒就能实现,所以这里我们只讨论 根节点大于所有直接相邻的子节点
我们考虑树上DP
状态设计为 ans[i][j] =
节点i在它和它的所有子树节点中,排序从小到大刚好为j的方案数
对于一个选定的节点,有子树T1,T2,T3,T4,…
设它们的根节点为r1,r2,r3,r4,…
设它们的节点个数为s1,s2,s3,s4,…
那么, 我们考虑维护合并这些子树.
因为 我们这里考虑的是 当前节点要大于所有子树的根节点
所以 合并的时候维护 dp[i] =
已合并的子树中,根节点最大值恰好为i
的方案数
以T1和T2为例
分成两种, T1的根更大,和T2的根更大(可以对称处理)
下面讨论 T1根更大
设r1在T1 中的位置为i(1-index)
合并后的序列为(i-1个T1中的点 和 j个T2中的点) r1 ((s1-i)个T1中的点 和 (s2-j)个T2中的点)
注意在两个子树分别的内部,顺序已经固定, 所以只有交叉的顺序不定
方案为 $C((i-1)+j,i-1) \cdot C((s1-i)+(s2-j),s1-i) \cdot ans[r1][i]$
再考虑, T2中的r2的位置,因为T1根更大,所以r2只能在前j个中
$dp[i+j] += C(i+j-1,i-1) \cdot C(s1+s2-i-j,s1-i) \cdot ans[r1][i] \cdot (ans[r2][1]+\cdots +ans[r2][j])$
右边的和可以前缀和O(1),
组合数可以预处理递推O(1),
所以
我们通过 循环一遍T1的节点数,再一遍T2的节点数数,就能得到dp, 每个+=
是O(1)
这里总代价n2,而我错误估计以为n3就没写,哭了
乍看上去,所有节点处理一次,每个节点是子树之间合并,个数的乘积是n3复杂度
实际上,考虑贡献, 一次合并子树的两个子树节点个数乘积大小的代价, 相当于,两个子树之间的节点建立一个配对关系.又因为任何两个节点至多建立两次(讨论根更大)关系,所以总代价为$O(n^2)$
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| #include <bits/stdc++.h> using namespace std; typedef long long ll; #define MOD 998244353 #define rep(i, a, n) for (ll i = a; i < n; i++) int n; vector<int> p2[3010];
vector<vector<ll> > ans(3010, vector<ll>(1, 0)); ll qC[3010][3010]; void mergeTable(const vector<ll>& arr0, const vector<ll>& arr1, vector<ll>& res) { vector<ll> presum(arr1.size(), arr1[0]); rep(i, 1, arr1.size()) { presum[i] = (arr1[i] + presum[i - 1]) % MOD; } rep(i, 0, arr0.size()) { rep(j, 0, arr1.size()) { (res[i + j + 1] += (((arr0[i] * qC[i + j + 1][i]) % MOD * qC[arr0.size() + arr1.size() - i - j - 2][arr0.size() - i - 1]) % MOD * presum[j]) % MOD) %= MOD; } } } void dfs(int idx, int fa, int g) { vector<ll> dp = {}; for (auto item : p2[idx]) { if (item == fa) continue; dfs(item, idx, g ^ 1); if (g) { reverse(ans[item].begin(), ans[item].end()); } if (!dp.size()) { dp = ans[item]; } else { vector<ll> res(dp.size() + ans[item].size(), 0); mergeTable(dp, ans[item], res); mergeTable(ans[item], dp, res); dp = res; } } ans[idx] = vector<ll>(dp.size() + 1, 0); if (dp.size() == 0) { ans[idx][0] = 1; } else { ll cnt = 0; rep(i, 0, dp.size() + 1) { ans[idx][i] = cnt; (cnt += dp[i]) %= MOD; } } if (g) { reverse(ans[idx].begin(), ans[idx].end()); } } int main() { rep(i, 0, 3001) { rep(j, 0, i + 1) { qC[i][j] = j == 0 ? 1 : (qC[i - 1][j] + qC[i - 1][j - 1]) % MOD; } } scanf("%d", &n); rep(i, 0, n - 1) { int u, v; scanf("%d %d", &u, &v); p2[u].push_back(v); p2[v].push_back(u); } dfs(1, 0, 0); ll res = 0; rep(i, 0, n) { (res += ans[1][i]) %= MOD; } printf("%lld\n", (2 * res) % MOD); return 0; }
|
submissions
官方题解
总结
- 可以当作知识点的: 子树全部 节点数相乘的代价,总代价是$O(n^2)$
- 复杂度也可以在纸上不要脑补
E
https://atcoder.jp/contests/arc130/tasks/arc130_e
一个数组,每次选最小值中的一个+1,记录下标。
形成了一个下标数组。
现在给你下标数组,求满足下标数组的字典序最小的原数组
题解
题目样例为例
1 1 4 4 2 1
如果 我们能给他们分组
1 | 1 4 | 4 2 1
那么,很容易知道合法的最小结果
1 (操作前大小为1)| 1 4 (操作前大小为2) | 4 2 1 (操作前大小为3)
我们只关心
1 | 1 4 (操作后所有为3)
所以原数组为
3-2 3-0 3-0 3-1
= 1 3 3 2
所以核心问题变成去找这个层之间的分割线
见下面代码中的注释
last(pos) =
这个位置值相同的值的上一个位置
cur[value] =
位置,辅助计算last的数组
cnt
当前层的个数
mx
上一层至少的结束的位置/当前层至少的起始位置
f(pos)=
pos是有效的层级分割值,或者-1
代码
jiangly 大佬的代码 加的注释的
https://atcoder.jp/contests/arc130/submissions/27614814
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include <bits/stdc++.h> using namespace std; typedef long long ll; #define rep(i, a, n) for (ll i = a; i < n; i++)
int main() { int N, K; scanf("%d %d", &N, &K); vector<int> i(K); rep(k, 0, K) { scanf("%d", &i[k]); i[k]--; } vector<int> last(K); vector<int> cur(N, -1); rep(k, 0, K) { last[k] = cur[i[k]]; cur[i[k]] = k; } vector<int> f(K + 1, 0); int cnt = 0; int mx = -1; rep(k, 0, K) { mx = max(mx, last[k]); if (last[k] == -1) { cnt++; } f[k + 1] = (mx >= k + 1 - cnt || f[k + 1 - cnt] == -1) ? -1 : 1 + f[k + 1 - cnt]; } int c = -1; int len = -1; rep(k, mx + 1, K + 1) { if (f[k] >= 0 && (c == -1 || c >= f[k])) { c = f[k]; len = k; } } if (c == -1) { cout << "-1\n"; return 0; } vector<int> A(N, c + 1); rep(k, 0, len) { A[i[k]]--; } rep(i, 0, N) { printf("%d%c", A[i], " \n"[i == N - 1]); } return 0; }
|
总结
跳表什么都会,但是能把细节分析出来,感觉是熟练度而不是什么缺失的知识点。属于这次学会了,下次可能还是不会的题。