模拟赛 R22
T3(P10053)
题目描述
你有一个玩具,玩具里面有 \(n\) 个槽,编号 \(1\) 到 \(n\),每个槽可以容纳一个小球,槽之间形成一棵有根二叉树。根在最上方,叶子在最下方,根固定为 \(1\) 号。
现在你要往玩具里的某些槽位放球,球被放入之后会自动向叶子方向下落。具体来说,一个球在树上的下落规则是:
- 如果当前位置是叶子或者所有儿子上都有球,则停止下落。
- 如果恰有一个空儿子,则落到那个空儿子上。
- 如果有两个空儿子,且球不是从更上层落下来的,则会在两个儿子上等概率随机选一个下落。
- 如果有两个空儿子,且球是从上一层落下来的,则球会落到和当前下落方向相同的儿子上。(也就是说,如果当前位置是上一层的左儿子,那么就还是落到左儿子;如果当前位置是上一层的右儿子,那么就还是落到右儿子。)
一个球被放入之后会一直下落直到当前位置是叶子或者所有儿子上都有球为止。
现在你有 \(k\) 个球,其中第 \(i\) 个球必须放进 \(p_i\) 号槽,且你每次放置一个球后必须等待停止下落后才能放下一个,但你可以任意决定这 \(k\) 个球的放入顺序。如果放 \(i\) 号球的时候 \(p_i\) 号槽已经有球了,那么就不能放了。
问:在所有 \(k\) 个球都能成功放入的前提条件下,有多少种可能的不同的最终结果。答案对 \(10^9+7\) 取模。
两个最终结果不同当且仅当所有球全部放入和下落完毕后,存在一个槽位上球的有无或者编号不同。
Solution
状态:\(f_{i, j}\) 表示节点 \(i\) 的子树,有 \(j\) 个父亲来的球的方案数。
这里有一个贡献延后计算的 trick,我们把这个放状态里,认为 \(j\) 个球到 \(i\) 子树内时已经在父亲处选好并且决定好了顺序,只用决定它们和当前位置球的相对顺序。然后我们把一些基本的信息算一下:\(a_i\) 表示在 \(i\) 处放了几个球,\(b_i\) 表示子树内的 \(a_i\) 之和。
如何转移?首先我们现在有 \(a_i + j\) 个球。我们考虑枚举滚进先子树(对于那些从父亲来的求,如果 \(i\) 本身是左子树,就优先滚进左子,否则优先滚进右子,我们称优先的这个是先子树)的个数 \(p\)。相当于,对于一个长为 \(a_i + j\) 的序列,我们认为前 \(p\) 个滚进先子树 \(x\),后 \(q = a_i + j - p\) 个滚进后子树 \(y\)。然后我们发现,如果这个放球的顺序不同,那最终的方案肯定不同。那么就可以通过计数这个序列来转移了。
如果 \(p + b_x = siz_x\) 也就是说先子树被填满了,此时对序列没有任何限制,\(f_{i, j} \gets f_{i, j} + {a_i + j \choose a_i} (a_i)! \times f_{{x},{p}} \times f_{{y},{q}}\)。
否则,说明先子树还没被填满,此时父亲来的 \(j\) 个球必须放在前 \(p\) 个,因为他们优先滚入先子树,也就是 \(f_{i, j} \gets f_{i, j} + {p \choose j} (a_i)! \times f_{{x},{p}} \times f_{{y},{q}}\)。
把每次循环的上下界取好,复杂度可以做到 \(O(n^2)\) 级别,实际根本跑不满。
关键的观察就是子树内的已经填好的话,最后的分布情况只跟滚到这个点的所有球的顺序有关。我们要做的只是抉择上面滚下来的球的相对顺序,这样就等价于把他们的位置选出来了。
注意,当左右子树都被填满时,也有可能滚到本节点,写得时候记得特判。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 4e3 + 5, mod = 1e9 + 7;
int n, k, ls[N], rs[N], a[N], siz[N], b[N];
ll f[N][N], c[N][N], fac[N];
void add(ll &x, ll y){ (x += y) %= mod; }
void dfs(int u, bool op){
if(ls[u]) dfs(ls[u], 0);
if(rs[u]) dfs(rs[u], 1);
b[u] = b[ls[u]] + b[rs[u]] + a[u];
siz[u] = siz[ls[u]] + siz[rs[u]] + 1;
int x = (op == 0 ? ls[u] : rs[u]), y = (op == 0 ? rs[u] : ls[u]);
for(int j = 0; j <= min(siz[u], k) - b[u]; ++j){
for(int p = j; p <= a[u] + j; ++p){
int q = a[u] + j - p;
if(p + b[x] > siz[x]) break;
if(p + b[x] == siz[x] && q + b[y] > siz[y]) --q;
if(q + b[y] > siz[y]) continue;
if(p + b[x] != siz[x]) add(f[u][j], f[x][p] * f[y][q] % mod * c[p][j] % mod * fac[a[u]] % mod);
}
int p = siz[x] - b[x], q = a[u] + j - p;
if(q + b[y] > siz[y]) --q;
if(q + b[y] > siz[y]) continue;
add(f[u][j], f[x][p] * f[y][q] % mod * c[j + a[u]][a[u]] % mod * fac[a[u]] % mod);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
c[0][0] = fac[0] = 1;
for(int i = 1; i <= n; ++i){
c[i][0] = 1;
fac[i] = fac[i - 1] * i % mod;
for(int j = 1; j <= i; ++j)
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
}
for(int i = 1; i <= k; ++i){
int p; cin >> p;
a[p]++;
}
for(int i = 1; i <= n; ++i) cin >> ls[i] >> rs[i];
f[0][0] = 1;
dfs(1, 0);
cout << f[1][0] << '\n';
return 0;
}
T4(QOJ7784)
题目描述
奶龙要踏上伟大旅程!
奶龙的旅途会涉及 \(n\) 块区域,每个区域都存在一种魔法。第 \(i\) 块区域的魔法价值为 \(w_i\)。
但是并非任意两个区域都直接有道路相连。实际上,仅存在 \(n - 1\) 条道路连接了这 \(n\) 个区域。任意两个区域都可以通过这 \(n - 1\) 条道路相互到达。
奶龙会进行若干次旅行。对于每一次旅行,奶龙会先选择一个之前的旅行没有到过的区域作为起点,然后沿着道路四处游走。奶龙 不能经过之前的旅行到过的区域。但是 可以重复经过本次旅行到过的区域。奶龙 第一次经过某个区域便会收集这个区域的魔法,之后再经过便不会收集。对于一次旅行,奶龙定义它的价值为 本次旅行收集到的魔法的价值的次大值(非严格次大)。如果一次旅行只收集到了一种魔法,那么这次旅行的价值为 \(0\)。
可爱的奶龙还给你举了一些例子:
如果一次旅行只收集到了一种价值为 \(3\) 的魔法,那么这次旅行的价值为 \(0\)。
如果一次旅行收集到了三种价值分别为 \(1\), \(2\),\(3\) 的魔法,那么这次旅行的价值为 \(2\)。
如果一次旅行收集到了四种价值分别为 \(1\),\(2\),\(3\),\(3\) 的魔法,那么这次旅行的价值为 \(3\)。
现在,奶龙想让你帮他安排旅行路线,使得在收集完所有魔法的前提下,最大化所有旅行的价值的和。
Solution
首先要观察出这个划分连通块是假的,实际上是让我们划分链。因为对于一个连通块的贡献,我们可以把这个连通块的最大值和次大值所在的点拿出来,把他们之间连一条链,发现这样不改变答案但是释放出来更多的可以被划分。跟进一步地,这些链的端点一定是最大值 & 次大值。
问题就变成了在树上划分若干条链,每条链的贡献是端点的 \(min\),最大化贡献和。
这种类焊接问题已经见了很多次了,状态基本都是 \(f_{u, \cdots}\) 表示子树内有一条半链和 \(g_u\) 表示子树内已经划分完了。这里我们用 \(f_{u, i}\) 表示子树内有一条一头为 \(i\) 的半链的贡献之和的最大值,\(g_u\) 表示所有点都划分完了的贡献之和的最大值。有转移
\(g_w\) 是之前的某个子树。
然后这个转移可以线段是合并优化,上面那个 \(f'_{u, i} + f_{v, j} + \min\{i, j\}\) 也可以用类似线段树合并的办法来求。就是把左孩子的 \(\max\{f_i + i\}\),和右孩子的 \(f_j\) 加起来。然后下面的转移有一个全局加的形式,所以要维护 \(f_i\),\(f_i + i\) 的最大值,支持打 tag 的线段树。
离散化后做,复杂度 \(O(n \log n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
typedef long long ll;
int tot, ls[N << 6], rs[N << 6], rt[N], n, siz;
ll mx[N << 6], mxp[N << 6], tag[N << 6], w[N], V[N], g[N];
vector<int> e[N];
void chmax(ll &x, ll y){ x = max(x, y); }
void pushup(int p){
mx[p] = max(mx[ls[p]], mx[rs[p]]);
mxp[p] = max(mxp[ls[p]], mxp[rs[p]]);
}
void addtag(int p, ll k){
if(!p) return;
tag[p] += k;
mx[p] += k;
mxp[p] += k;
}
void pushdown(int p){
if(tag[p]){
addtag(ls[p], tag[p]);
addtag(rs[p], tag[p]);
tag[p] = 0;
}
}
void upd(int &p, int x, ll w, int pl = 1, int pr = siz){
p = ++tot;
if(pl == pr) return mx[p] = 0, mxp[p] = w, void();
int mid = (pl + pr) >> 1;
if(x <= mid) upd(ls[p], x, w, pl, mid);
else upd(rs[p], x, w, mid + 1, pr);
pushup(p);
}
ll qry(int u, int v, int pl = 1, int pr = siz){
if(!u || !v) return 0;
if(pl == pr){
ll ret = max(mx[u] + mxp[v], mx[v] + mxp[u]);
return ret;
}
pushdown(u), pushdown(v);
ll ret = max(mxp[ls[u]] + mx[rs[v]], mxp[ls[v]] + mx[rs[u]]);
int mid = (pl + pr) >> 1;
chmax(ret, qry(ls[u], ls[v], pl, mid));
chmax(ret, qry(rs[u], rs[v], mid + 1, pr));
return ret;
}
void merge(int &u, int v, int pl = 1, int pr = siz){
if(!u || !v) return u = u + v, void();
if(pl == pr){
chmax(mx[u], mx[v]);
chmax(mxp[u], mxp[v]);
return;
}
pushdown(u), pushdown(v);
int mid = (pl + pr) >> 1;
merge(ls[u], ls[v], pl, mid);
merge(rs[u], rs[v], mid + 1, pr);
pushup(u);
}
void dfs(int u, int fa){
upd(rt[u], w[u], V[w[u]]);
g[u] = 0;
ll smg = 0;
for(int v : e[u]){
if(v != fa){
dfs(v, u);
g[u] = max(g[u] + g[v], qry(rt[u], rt[v]));
addtag(rt[u], g[v]);
addtag(rt[v], smg);
merge(rt[u], rt[v]);
smg += g[v];
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> w[i], V[i] = w[i];
for(int i = 1; i < n; ++i){
int u, v; cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
sort(V + 1, V + 1 + n);
siz = unique(V + 1, V + 1 + n) - V - 1;
for(int i = 1; i <= n; ++i) w[i] = lower_bound(V + 1, V + 1 + siz, w[i]) - V;
mx[0] = mxp[0] = -1e18;
dfs(1, 0);
cout << g[1] << '\n';
return 0;
}
Summary
先写后想依旧是原则。就是 T3 那个 30 的部分分,脑袋里全想的 dp,实际上有非常好写+好像的组合做法。导致 T4 最后半个小时只写了个 10 pts 的。
有时候部分分的复杂度可能很优。

浙公网安备 33010602011771号