QOJ7895 图划分 2
原题链接:Graph Partitioning 2
分析
嗯?好像摸到了 DP,就是说,我们发现,对于那些子树大小(保留的连通块)已经是 k + 1 k+1 k+1 的,不得不切了;而对于那些恰好为 k k k 的,则需要再考虑考虑。这就会对答案产生 2 2 2 的贡献。我们可以设 d p u , s i z dp_{u,siz} dpu,siz 表示 u u u 子树保留 s i z siz siz 大小的连通块(剩下全都切成了 k + 1 k+1 k+1 或者 k k k 的块)的方案数。
这个显然是 O ( n 2 ) O(n^2) O(n2) 的,我并不认为它对答案有什么启发性......
不过可以尝试一下转移,就是:
d p u , s i z = ∏ v ∈ s o n u ( d p v , k × d p v , k + 1 × d p v , s i z − 1 ) dp_{u,siz}=\prod\limits_{v\in son_u}(dp_{v,k}\times dp_{v,k+1}\times dp_{v,siz-1}) dpu,siz=v∈sonu∏(dpv,k×dpv,k+1×dpv,siz−1)
这对吗?感觉不是特别对。
那不如直接钦定,我们直接设 d p u , 0 / 1 / 2 dp_{u,0/1/2} dpu,0/1/2 表示 u u u 子树现在保留的大小是 k / k + 1 / o t h e r s k/k+1/others k/k+1/others,然后转移。但是,感觉这个还是 O ( n 2 ) O(n^2) O(n2) 的......反正这个转移挺贪心的吧......感觉还是需要大力分讨降低复杂度。
上个厕所继续思考。上完厕所还是不会......要相信自己可以写出来,加油,继续思考,我们九点🕘的时候看题解。
显然,我们在处理完子树之后,会剩下一条链,然后,每 k k k 个捆一下,剩下的似乎可以直接插板,然后求 2 2 2 的整数次幂即可。
那么不如直接设 d p u , 0 / 1 dp_{u,0/1} dpu,0/1 表示 ( u , f a u ) (u,fa_u) (u,fau) 不切/切的方案数。
那么,转移有:
d p u , 0 = ∑ v ∈ s o n u ( d p v , 1 + d p v , 0 ) d p u , 1 = ⌊ s z u k ⌋ + s z u % k + ⌊ n − s z u k ⌋ + ( n − s z u ) % k dp_{u,0}=\sum\limits_{v\in son_u}^{}{(dp_{v,1}+dp_{v,0})}\\ dp_{u,1}=\left\lfloor\dfrac{sz_u}{k}\right\rfloor+sz_u\%k+\left\lfloor\dfrac{n-sz_u}{k}\right\rfloor+(n-sz_u)\%k dpu,0=v∈sonu∑(dpv,1+dpv,0)dpu,1=⌊kszu⌋+szu%k+⌊kn−szu⌋+(n−szu)%k
答案应该就是:
a n s = d p 1 , 0 + d p 1 , 1 ans=dp_{1,0}+dp_{1,1} ans=dp1,0+dp1,1
但是,这完全违背了我求取 2 2 2 的整数次幂的直觉。准备看题解吧。
哦不不不不不不,这无疑是令人愤怒的。我们发现,第一次的状态是对的。
转移有:
d p u , i = d p u , i × ( d p v , k + d p v , k + 1 ) d p u , i + j = d p u , i + d p v , j ( i + j ≤ k + 1 ) dp_{u,i}=dp_{u,i}\times(dp_{v,k}+dp_{v,k+1})\\ dp_{u,i+j}=dp_{u,i}+dp_{v,j}(i+j\le k+1) dpu,i=dpu,i×(dpv,k+dpv,k+1)dpu,i+j=dpu,i+dpv,j(i+j≤k+1)
总之就是两颗子树合并吧,看着理解就行。
然后,我们发现这个复杂度是 O ( n k 2 ) O(nk^2) O(nk2) 的,如果我们使用 vector<pair<int, int>> 可以优化到 O ( n ) O(n) O(n),又由于这个合并时需要排序,所以我们可以获得 O ( n l o g n ) O(nlogn) O(nlogn) 的复杂度。
正解
cpp
#include <bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
const int N = 200005;
int T;
int n, k;
struct node{
int siz, val;
};
vector<node> dp[N];
bool cmp(node tmp1, node tmp2){
if (tmp1.siz == tmp2.siz)
return tmp1.val < tmp2.val;
return tmp1.siz < tmp2.siz;
}
vector<int> e[N];
int sz[N];
void dfs(int u, int fa){
sz[u] = 1;
dp[u].push_back({1, 1});
for (auto v : e[u]){
if (v == fa)
continue;
dfs(v, u);
sz[u] += sz[v];
}
for (auto v : e[u]){
if (v == fa)
continue;
int cnt = 0;
for (node tmp : dp[v]){
if (tmp.siz == k)
cnt = (cnt + tmp.val) % mod;
if (tmp.siz == k + 1)
cnt = (cnt + tmp.val) % mod;
}
vector<node> cur;
for (node tmp1 : dp[u]){
if (tmp1.val == 0)
continue;
if (cnt > 0){
cur.push_back({tmp1.siz, tmp1.val * cnt % mod});
}
for (node tmp2 : dp[v]){
if (tmp2.val == 0)
continue;
if (tmp1.siz + tmp2.siz > k + 1)
continue;
cur.push_back({tmp1.siz + tmp2.siz, tmp1.val * tmp2.val % mod});
}
}
sort(cur.begin(), cur.end(), cmp);
dp[u].clear();
for (node tmp : cur){
if (!dp[u].empty() && dp[u].back().siz == tmp.siz){
dp[u].back().val = (dp[u].back().val + tmp.val) % mod;
}
else{
dp[u].push_back(tmp);
}
}
}
}
void init(){
for (int i = 1; i <= n; i++){
e[i].clear();
dp[i].clear();
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> T;
for (int cs = 1; cs <= T; cs++){
init();
cin >> n >> k;
for (int i = 1, u, v; i < n; i++){
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1, 0);
int ans = 0;
for (node tmp : dp[1]){
if (tmp.siz == k){
ans = (ans + tmp.val) % mod;
}
if (tmp.siz == k + 1){
ans = (ans + tmp.val) % mod;
}
}
cout << ans << "\n";
}
}
LGP9084 PA 2018 Skwarki
原题链接:PA 2018 Skwarki
分析
现尝试用一个 DP 表示一下。不保证 P P P 是质数吗?
形式化的,有多少个排列满足进行 K K K 次操作,删去谷, K K K 次操作之后只剩下一个元素。
那么,剩下的这个元素是谁似乎很明显是最大值。
任何排列似乎都可以根据以上删谷的操作变成一个元素......
所以,我们似乎有一个最大的 K K K 和一个最小的 K K K,我们尝试研究一下。
我们是不是可以证明 log 2 n ≤ K \log_2n\le K log2n≤K?
那么 K K K 的最大值呢?如果恰好有序的话,不对,如果严格降序是不能通过以上操作进行的。
所以,在严格升序的情况下,我们可以得知, K ≤ n − 1 K\le n-1 K≤n−1。
然后呢?我们就知道了,那么,这个 K K K 究竟要怎样消耗呢?
而且,我们知道,总的谷的数量只能是 n − 1 n-1 n−1 吧......那是不是可以做类似于背包的东西?这个不是很准确,但是感觉是可以做的。
非要笛卡尔树吗?我怒了。了解一下定义吧。好的,看完了。
D D D 老师又发力了!
| 你的思路/猜想 | 修正后的准确表述 |
|---|---|
| 最后剩下的元素是最大值 | 正确。全局最大值不可能成为谷,最终幸存者只能是它。 |
| 总谷的数量是 n − 1 n-1 n−1 | 应改为:累计被删掉的元素数 / 累计"删谷次数"是 n − 1 n-1 n−1 。初始谷数不一定是 n − 1 n-1 n−1,谷是动态产生的;而 K K K 是轮数,不是删除总数。 |
| 任何排列都能通过删谷变成只剩一个元素 | 不正确。有些排列无法一路删到只剩最大值;能否成功取决于排列/笛卡尔树结构。 |
| log 2 n ≤ K \log_2 n \le K log2n≤K | 需要限定,不能和"删除总数 n − 1 n-1 n−1"混用 。 K K K 是轮数,轮数的上下界取决于每轮删谷规则和笛卡尔树形态,不能简单说成普遍成立的绝对结论。 |
| 严格升序时 K ≤ n − 1 K\le n-1 K≤n−1 | 这个例子要修正 。严格升序不是" K K K 很大"的好例子:它内部元素都是谷,一轮会删很多,且不一定能删到只剩一个。 K K K 接近 n − 1 n-1 n−1 的构造应另找特定笛卡尔树形态,不能拿严格升序说明。 |
| "类似背包"的 DP | 直觉正确 。标准做法是笛卡尔树上的合并 DP,以轮数 K K K 作为"容量"维度;左右子树并行删,合并时轮数取 max \max max 并处理根,而不是把 n − 1 n-1 n−1 个删除数直接相加分配给 K K K。 |
| 谷的总数可以当作背包容量 | 应改为:背包容量维度是轮数 K K K ; n − 1 n-1 n−1 是总删除个数,不是直接拿来分给 K K K 的静态谷数。 |
好的,那么,我们究竟该怎么写呢?呃,看一下题解吧。呃,我们不如找D老师先要一个暴力的 DP,看看怎么写。
我们发现,这个表格里学的是错的,不论怎样,我们都可以有解。
我们发现,最先删除的是笛卡尔树中没有两个儿子的点,因为儿子是局部谷,所以,结论显然成立。
由于最大值必然保留,所以,我们枚举最大值最后所处的位置,设 d p i , j , 0 / 1 dp_{i,j,0/1} dpi,j,0/1 表示长度为 i i i 的排列, j j j 次删空,没有/有边界的方案数。
看错题了!
并不是但谷,只需要保证存在相邻比它大的就可以了!然后,我们 K K K 的上届就变成了 log 2 n \log_2n log2n。
那不就是类似于笛卡尔树的形态吗?对吗?好吧,反正之前那个好理解多了。那我们依然设 d p i , j , 0 / 1 dp_{i,j,0/1} dpi,j,0/1 表示长度为 i i i 的排列, j j j 次删空,没有/有边界的方案数。然后,就可以做了。
这是啥啊😭
正解
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1005;
int n, k, p;
__int128 dp[N][N][2];
int C[N][N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> k >> p;
if (k > 10){
cout << 0;
return 0;
}
for (int i = 0; i < N; i++){
C[i][0] = 1;
for (int j = 1; j <= i; j++){
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % p;
}
}
dp[0][0][0] = dp[0][0][1] = 1;
for (int i = 1; i < n; i++){
for (int j = 1; j <= i; j++){
for (int k1 = 0; k1 <= k; k1++){
for (int k2 = 0, nxt; k2 <= k; k2++){
if (k1 == k2)
nxt = k1 + 1;
else
nxt = max(k1, k2);
dp[i][nxt][0] = (dp[i][nxt][0] + dp[j - 1][k1][0] * dp[i - j][k2][0] * C[i - 1][j - 1]) % p;
nxt = max(k1, k2 + 1);
dp[i][nxt][1] = (dp[i][nxt][1] + dp[j - 1][k1][1] * dp[i - j][k2][0] * C[i - 1][j - 1]) % p;
}
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++){
for (int k1 = 0; k1 <= k; k1++){
for (int k2 = 0; k2 <= k; k2++){
if (max(k1, k2) == k){
ans = (__int128)(ans + dp[i - 1][k1][1] * dp[n - i][k2][1] * C[n - 1][i - 1]) % p;
}
}
}
}
cout << ans;
}