南外集训Day6
由于我太菜了所以我很菜……、
还好集训分两次,中间给了我时间来补这些博客。
不过话说这些南外的老师似乎特别喜欢搞 $CF$ 的题过来给我们做,还改题面,但是幸好他们的改题面技术不如我,所以赛后被我把中文翻译成英文搜到了原题……
我太菜了!
通过的题
提一下吧,第一题因为题目没看清交了 $5$ 发才过,很不应该(虽然没有罚时)。第二题一发即过。第三题因为用了已死的 puts
,然后本地过了样例,在线测评一直错,然后我懵了整整 $20$ 分钟。
会但是没通过的题
$D$ 是因为数据出锅了,很想揍出题人但是无奈找不到人。而且赛后没有人来修! $E$ 题赛后订正时发现数据输入终于没有出锅了!然后转眼发现标程锅了,所以数据输出就错了……因为当 $k$ 大于颜色种类数时答案是 $0$ ,但是标程没特判!
不会的题
F
原题。
首先审清题意:这是完全图!!!
赛时想到的超级接近正解,但是因为当时觉得很麻烦就没有打了,但实际上再理一理思路之后会发现非常简单。
赛时首先就想到了一个状态, $dp[i][j]$ 表示前 $i$ 个数,所有点与 $1$ 连边的最大值为 $j$ 的方案数,但是发现当下一个与 $1$ 的边小于上一个时没法转移!然后考虑我们可以按连接 $1$的边权从小到大地取,那么边权一样的必定是连续的一端,那么考虑枚举第一维,第二维做个前缀和优化即可,由于最后要乘上 $n!$ (因为点有编号), 所以中间每个地方要除去重复的数的阶乘。
正如上所述,事实正解和这个的方法基本一样,但只是把我两维的顺序 $swap$ 一下,且就不用前缀和优化。因为考虑我么中间可以跳过不选。我们可以通过题意得如果我们取所有与1相连的边,可以得到最小生成树之一(也有可能是唯一一个),按照边权从小到达来取,中间乘上组合数计算方案即可。
给出简单的DP式:$$ dp_{i,j}=\sum_{t=0}^{j-1}dp_{i-1,t}*C_{n-1-t,j-t}*(k-i+1)^{t*(j-t)+(j-t)*(j-t-1)/2} $$
其中那个组合数的意义是从 $n-1-t$ 个点中任选 $j-t$ 个的方案,其中 $n-1-t$ 是因为 $1$ 号店已经固定了是因为 $1$ 号店已经固定在第 $1$ 个了。最后的 $t*(j-t)+(j-t)*(j-t-1)/2$ 次方是把集合内乘集合外的加上集内两两之间的方案数。
考虑到 $t=j$ 的情况,我们直接把方案数加上上一个的方案数即可。$$ dp_{i,j}+=dp_{i-1,j} $$
最后赞一下,这道题很好地又考察了我们的 $dp$ ,有考察了我们对一些传统算法的理解。
#include <bits/stdc++.h>
#define ll long long
#define L(i, a, b) for(int i = a; i <= b; i++)
#define R(i, a, b) for(int i = a; i >= b; i--)
using namespace std;
const int N = 251;
const ll mod = 998244353;
int n, k, c[N][N], p[N][N * N], dp[N][N];
int main(){
scanf("%d%d", &n, &k);
c[0][0] = 1;
L(i, 1, n){
L(j, 0, n){
if(!j) c[i][j] = c[i - 1][j];
else c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
}
}
L(i, 1, k){
p[i][0] = 1;
L(j, 1, n * n)
p[i][j] = 1ll * p[i][j - 1] * i % mod;
}
dp[0][0] = 1;
L(i, 1, k){
L(j, 0, n - 1){
dp[i][j] = dp[i - 1][j];
L(t, 0, j - 1)
dp[i][j] = (dp[i][j] + 1ll * dp[i - 1][t] * c[n - 1 - t][j - t] % mod * p[k - i + 1][t * (j - t) + (j - t) * (j - t - 1) / 2] % mod) % mod;
}
}
printf("%d\n", dp[k][n - 1]);
return 0;
}
G
原题。
这道题的套路是两种经典套路相结合。
- 对于一类二分题,我们可以在二分后把满足条件的设为 $1$ ,不满足的设为 $0$ 或 $-1$ (视情况而定),这时我们能很方便与统计/做前后缀和。
- 对于一类动态的前后缀和问题(或者上述问题),很多时候可以用线段树维护动态的。
我太菜了,第一种套路都没想到。
所以到底该怎么做呢?我们先从简单入手。假设我们已经确定了哪些位置有炸弹,那么可以套用第一种套路。就是去二分答案 $x$ ,这时候我们发现我们把大于 $x$ 的全部标为 $1$ ,表示可以删掉,不难发现,由于 $x$ 只要剩 $1$ 个,而且相同的权值把越前面置 $1$ 必定更优(那么后面就越可能有东西能删),那么我们把除最后一个外权值等于 $x$ 的也置 $1$ ,然后判断是否有前缀和小于等于 $0$ 即可。
由于这种做法非常的逊,所以我们考虑优化他(对每个数做一遍的时间复杂度为 $O(n^2logn)$ )。所以我们考虑优化掉一个 $n$ 的级别。这时也许还是被二分限制着,但是只要阅读了套路二,你将会豁然开朗。
由于有单调性所以能二分,而我们发现答案是单调递减的,所以呢我们发现在这样的性质下一个答案是合法的仅当有一个点右边的大于等于 $ans$ 的数大于炸弹数,这样必定会剩下至少一个没有被删,而因为答案是单调递减的所以答案一定是当前的。我们记录每种数值的位置(注意数值是 $1$ 到 $n$ 的全排列),每次,把 $1$ 到 $q[i-1]$ 的位置置位减 $1$ ,然后把答案减小时对 $1$ 到答案在 $p$ 中的位置加 $1$ ,这样可以算出后缀和。
#include <bits/stdc++.h>
#define L(i, a, b) for(int i = a; i <= b; i++)
#define R(i, a, b) for(int i = a; i >= b; i--)
using namespace std;
const int N = 300010;
struct Tree{
int l, r, mid, mx, lazy;
void Tag(int v){mx += v, lazy += v;}
}t[N << 2];
int n, id[N], p[N], q[N];
void Pushup(int p){
t[p].mx = max(t[p << 1].mx, t[p << 1 | 1].mx);
}
void Pushdown(int p){
t[p << 1].Tag(t[p].lazy);
t[p << 1 | 1].Tag(t[p].lazy);
t[p].lazy = 0;
}
void Build(int p, int l, int r){
t[p] = {l, r, l + r >> 1, 0, 0};
if(l == r) return;
Build(p << 1, t[p].l, t[p].mid);
Build(p << 1 | 1, t[p].mid + 1, t[p].r);
Pushup(p);
}
void Update(int p, int l, int r, int v){
if(l <= t[p].l && t[p].r <= r){
t[p].Tag(v);
return;
}
Pushdown(p);
if(l <= t[p].mid) Update(p << 1, l, r, v);
if(t[p].mid < r) Update(p << 1 | 1, l, r, v);
Pushup(p);
}
int main(){
scanf("%d", &n);
L(i, 1, n) scanf("%d", &p[i]), id[p[i]] = i;
L(i, 1, n) scanf("%d", &q[i]);
Build(1, 1, n);
int x = n;
L(i, 1, n){
if(i > 1) Update(1, 1, q[i - 1], -1);
while(t[1].mx <= 0) Update(1, 1, id[x--], 1);
printf("%d ", x + 1);
}
return 0;
}
H
原题。
这道题是个简单题,但是由于我太菜了所以没做出来。
根据数据范围,发现是个 $O(n^2)$ 的题。再一看,直接贪心似乎不行,因为你考虑假设现在断开了一个联通块,并且它是好的,但是假设把这个联通快再多加入一些节点,它还是好的,那么它可能让答案更优,因为当前已经选的联通快变少了,那么剩下可以分的联通块数也变多了,所以其它节点可能能创造更多好的联通快。那么不是贪心,也找不到什么特别好的性质,并且是一类最优解问题,不难想到动规。
我们再一思考,这道题是在树上,树上 $n^2$ 的动规有哪几种可能?
- 其一,最简单的,一维状态,由子节点或者父节点转移过去。但是显然你想不太到这么简单的方案;
- 二维的状态,那第二维该弄个什么好呢?发现好像只能是块的个数。但是发现这样就变成树上背包了,也就是下面那种。
-
那就是树上背包。
我们来具体讲一下怎么树上背包。首先状态不说,肯定是 $f[u][i]$ 表示以 $u$ 这个节点为根的子树分成 $i$ 块,不算 $u$ 所处的那一块的最大值,然后我们称一个点的权值为 $a_i$ ,其等于 $w_i - b_i$ 。然庵后我们经思考后,发现好像不能直接去树上背包,原因是你还是不知道当前根节点 $u$ 这一块的权值是多少。
当时我就愣住了,之后便认为这道题恐怖如斯,不是我能做出来的题。但这时候就应该去找一些性质或者贪心策略以使这个问题可以迎刃而解。所以我们找到了一个非常漂亮的性质(贪心策略):我们思考两种方案,第一种的块数比第二种多,但是第二种 $u$ 那个联通块的权值和大于第一种。
为了防止搞混两种,再清晰地理一下:
- 块数多
- $u$ 那个联通块的权值大
第一种一定 不劣于 第二种!
我们采取反证法,思考假设第二种比第一种优会优再什么情况下:可能存在第二种他的权值很大,然后把上边权值是非正数的一块变成正数了,然后会使答案加 $1$ ,那么我们思考第一种,他可以把权值是非正数的那块不去管他,那么他在最差情况下也就是第二种在最好情况下会和第二种一样。那么什么情况下第一种会比第二种优呢?就比如说 $u$ 上面的所有节点权值都是正数,那么第二种就会比第一种要差。
那么假设有两种方案块数相等,不难想到u那块必定是权值越大越好,因为可能越大。那么我们再用一个 $g[u][i]$ 表示在块数为 $f[u][i]$ 时 $u$ 那个联通块的最大权值。
最后给转移方程(这里用的是刷表法):
-
$v$ 所处的联通块不和 $u$ 合并,即单独划分
用 $f_{u,i}+f_{v,j}+(g_{v,j}>0),g_{u,i}$ 去更新 $f_{u,i+j},g_{u,i+j}$ 的最优值。
-
$v$ 的那个联通块与 $u$ 合并
用 $f_{u,i}+f_{v,j},g_{u,i}+g_{v,j}$ 去更新 $f_{u,i+j-1},g_{u,i+j-1}$ 的最优值。
边界是 $f[u][1]=0,g[u][1]=a_u$ 。
树上背包的时间复杂度 $O(n^2)$ 的证明见我的这篇博客。
其实这跑得过去是必然的,因为我们考虑最坏情况, $m$ 全是 $3000$ ,由于一句很重要的话: $n$ 之和不超过 $10^5$ ,所以我们发现,利用乘法分配律,最大可能是 $3*10^8$ 。而时限不紧,有整整 $4$ 秒,且动态规划常数算很小所以就这么跑过去了。
#include <bits/stdc++.h>
#define ll long long
#define L(i, a, b) for(int i = a; i <= b; i++)
#define R(i, a, b) for(int i = a; i >= b; i--)
using namespace std;
const int N = 3010;
int T, n, m, a[N], b[N], w[N], sz[N];
int f[N][N], r[N];
ll g[N][N], c[N];
vector<int>e[N];
void Init(){
memset(f, 0, sizeof(f));
memset(g, 0xf3, sizeof(g));
L(i, 1, n) e[i].clear();
}
void Dfs(int u, int pa){
f[u][1] = 0, g[u][1] = a[u];
sz[u] = 1;
for(int v: e[u]){
if(v == pa) continue;
Dfs(v, u);
L(i, 1, min(m, sz[u] + sz[v]))
r[i] = 0, c[i] = -3e12;
L(i, 1, min(m, sz[u])){
L(j, 1, min(m, sz[v])){
ll ff = f[u][i] + f[v][j] + (g[v][j] > 0), gg = g[u][i];
if(i + j <= m && ff > r[i + j] || ff == r[i + j] && gg > c[i + j])
r[i + j] = ff, c[i + j] = gg;
ff = f[u][i] + f[v][j], gg = g[u][i] + g[v][j];
if(i + j - 1 <= m && ff > r[i + j - 1] || ff == r[i + j - 1] && gg > c[i + j - 1])
r[i + j - 1] = ff, c[i + j - 1] = gg;
}
}
L(i, 1, min(m, sz[u] + sz[v]))
f[u][i] = r[i], g[u][i] = c[i];
sz[u] += sz[v];
}
}
void Solve(){
scanf("%d%d", &n, &m); Init();
L(i, 1, n) scanf("%d", &b[i]);
L(i, 1, n) scanf("%d", &w[i]);
L(i, 1, n) a[i] = w[i] - b[i];
int u, v;
L(i, 1, n - 1){
scanf("%d%d", &u, &v);
e[u].push_back(v), e[v].push_back(u);
}
Dfs(1, 0);
printf("%d\n", f[1][m] + (g[1][m] > 0));
}
int main(){
scanf("%d", &T);
while(T--) Solve();
return 0;
}