长春集训 Day 9 - DP 专题
长春集训 Day 9 - DP 专题
A1 - Centroids
Description
给定一棵大小为 \(n\) 的树,如果移除某个点后,树中每个连通分量的大小均不超过 \(\dfrac n2\),那么称该点为树的重心。你可以进行最多一次的如下操作:
- 从树中移除一条边,并插入一条新边,使新图仍为一棵树。
对于每个顶点,你需判断是否可以通过最多一次的操作使得该顶点成为树的重心。
\(2 \le n \le 4 \times 10^5\)。
Solution
这种“删边 - 加边”的问题,一般考虑换根 DP。
容易发现,一次操作等价于从原树的子树中切下一块,并将其接到另一个节点上。
因此,我们可以分情况讨论:
- 如果点 \(u\) 已经为原树的重心,那么无需操作;
- 如果点 \(u\) 不是重心,那么根据重心的性质,有且仅有一个与 \(u\) 相邻的子树(包含 \(u\) 的上部),使其大小严格大于 \(\dfrac n2\),且需要从这个子树中切下一个尽可能大的且大小不超过 \(\dfrac n2\) 的子树,并将其直接连接到 \(u\) 上。
现在问题转化为:对于树上的每个点 \(u\),如何快速求其祖先方向和儿子方向上,可以切下的最大合法子树块的大小。
设 \(dp_{u,0/1}\) 表示 \(u\) 的所有儿子子树中,能够切下的最大和次大合法子树块的大小。
一遍 DFS 可以求得每个子树的 \(siz\)。对于 \(u\) 的一个儿子 \(v\):
- 如果 \(siz_v \le \dfrac n2\),那么 \(v\) 子树整体就是一个合法块,大小为 \(siz_v\);
- 如果 \(siz_v \gt \dfrac n2\),那么不能切下整个 \(v\) 子树,但可以切下 \(v\) 子树内的最大合法块,大小为 \(dp_{v,0}\)。
上述判断逻辑提供了 \(dp\) 数组的候选值,取其最大值为 \(dp_{u,0}\),次大值(不严格)为 \(dp_{u,1}\) 即可。
同时,为了后续方便,我们需要记录:
- \(msub_u\),表示 \(u\) 的所有子节点中,子树最大的子节点的编号。
- \(mson_u\),表示提供 \(dp_{u,0}\) 的儿子 \(v\)。
再次 DFS 进行换根,计算来自祖先方向的贡献。
具体地,我们记 \(dp_{u,2}\) 表示在以 \(u\) 为根的树中,\(u\) 的祖先方向可以移除的最大合法块的大小。
换根时,当从父节点 \(u\) 转至子节点 \(v\) 时,我们需要为 \(v\) 计算出 \(u\) 的贡献。对于 \(v\) 来说,它的父侧部分由两部分组成:
- 原树中 \(u\) 的父侧部分;
- \(u\) 除 \(v\) 以外的其它子节点的子树。
考虑 \(dp_{v,2}\) 的计算。\(u\) 父侧和兄弟侧的区域大小为 \(n - siz_v\)。如果它小于等于 \(\dfrac n2\),那么整个上方区域就是一个合法块;否则,需要在上方区域中切下一个最大的合法块,而对于 \(v\) 来说,它不能使用 \(u\) 子树中属于自己的那部分,因此:
-
如果 \(v\) 恰为 \(u\) 的最大贡献儿子(即 \(mson_u = v\)),那么 \(v\) 能参考的兄弟子树的最大合法块是次大值 \(dp_{u,1}\);
-
否则可以参考最大值 \(dp_{u,0}\)。
同时,\(u\) 还能参考父侧贡献 \(dp_{u,2}\)。
综上,\(dp_{v,2}\) 需要权衡以上三种来源的最大值。
对于答案,只需考虑从子树内和子树外割边分别是否合法即可。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 4e5+5;
int n;
vector<int> g[N];
int siz[N];
int maxsub[N]; // 某节点的所有子节点中,最大子树的根的编号
int dp[N][3]; // 0-子树内最大值 1-子树内次大值(不严格) 2-子树外最大值
int maxson[N]; // 提供某节点子树内 dp0(最大值)的子节点编号
bool ans[N];
void DFS1(int u, int fa) {
siz[u] = 1;
dp[u][0] = dp[u][1] = 0;
maxson[u] = maxsub[u] = -1;
for (auto v : g[u]) {
if (v == fa) continue;
DFS1(v, u);
siz[u] += siz[v];
if (maxsub[u] == -1 or siz[v] > siz[maxsub[u]]) {
maxsub[u] = v;
}
int tmp = 0;
if (siz[v] <= n/2) {
tmp = siz[v];
} else {
tmp = dp[v][0];
}
if (dp[u][0] < tmp) {
dp[u][1] = dp[u][0];
dp[u][0] = tmp;
maxson[u] = v;
} else if (dp[u][1] < tmp) {
dp[u][1] = tmp;
}
}
}
void DFS2(int u, int fa) {
ans[u] = 1;
if (maxsub[u] != -1 and siz[maxsub[u]] > n/2) {
if (siz[maxsub[u]] - dp[maxsub[u]][0] > n/2) {
ans[u] = 0;
}
} else if (n - siz[u] > n/2) {
if (n - siz[u] - dp[u][2] > n/2) {
ans[u] = 0;
}
}
for (auto v : g[u]) {
if (v == fa) continue;
int tmp = 0;
if (n - siz[v] <= n/2) {
tmp = max(tmp, n - siz[v]);
} else {
tmp = max(tmp, dp[u][2]);
if (maxson[u] == v) {
tmp = max(tmp, dp[u][1]);
} else {
tmp = max(tmp, dp[u][0]);
}
}
dp[v][2] = tmp;
DFS2(v, u);
}
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n;
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v), g[v].push_back(u);
}
DFS1(1, -1);
DFS2(1, -1);
for (int i = 1; i <= n; i++) {
cout << ans[i] << ' ';
}
return 0;
}
B1 - Number of Multisets
Description
给定正整数 \(n,k\),求满足下列条件的多重集 \(S\) 的数量对 \(998244353\) 取模的结果:
- \(|S| = n,\ \sum\limits_{x\in S} x = k\);
- \(\forall x \in S, x = \dfrac 1{2^i}, i \in \mathbb{N}\)。
\(1 \le k \le n \le 3000\)。
Solution
注意到一个合法的多重集一定是对形如 \(\{1,1,1,\cdots\}\) 这样的集合的若干前缀乘上若干次 \(\dfrac 12\) 得到的。因此对于这样的集合,我们设 \(dp_{i,j}\) 表示考虑前 \(i\) 个元素,凑出和为 \(j\) 的方案数。
- 加入一个 \(1\) 时,有 \(dp_{i,j} \leftarrow dp_{i-1,j-1}\);
- 对前缀乘上 \(\dfrac 12\) 时,有 \(dp_{i,j} \leftarrow dp_{i,2j}\)。
显然这是完备的。答案为 \(dp_{n,k}\)。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 3005, mod = 998244353;
int n, k, dp[N][N];
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n >> k;
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = i; j >= 0; j--) {
dp[i][j] = dp[i-1][j-1];
if (j*2 <= i) dp[i][j] = (dp[i][j] + dp[i][j*2]) % mod;
}
}
cout << dp[n][k];
return 0;
}
C1 - Sonya and Problem Without a Legend 加强版
Description
给定长度为 \(n\) 的数组 \(a\),可以进行若干次如下操作:
- 选定一个元素,使其 \(+1\) 或 \(-1\)。元素在操作过程中可以为负。
试通过最少的操作次数,将数组变为严格递增的。
\(1 \le n \le 3 \times 10^5,\ 1 \le a_i \le 10^9\)。
Solution
考虑一个经典的 Trick:将每个元素减去对应的下标,可以使得限制从严格递增变为单调不降。具体地,记 \(b_i = a_i - i\)。后文将在 \(b\) 数组上操作。
先考虑朴素 DP。发现段的转移跟最后一位有关,所以设 \(f_{i}(t)\)(至于为什么要这样表示,后面你就懂了)表示仅考虑前 \(i\) 位,第 \(i\) 位元素恰好等于 \(t\) 的最少操作次数。
考虑转移,为了使得前 \(i-1\) 个数单调不降且 \(b_{i-1} \le b_i = j\),前 \(i-1\) 个数的状态一定是第 \(i-1\) 个数不超过 \(t\) 的最优情况。因此定义辅助数组 \(g_i(t)\) 表示前 \(i\) 位,第 \(i\) 位元素不超过 \(t\) 的最少操作次数。形式化地,有 \(g_i(t) = \min\limits_{y\le t} f_i(y)\)。
有了 \(g\) 之后,\(f\) 的转移是显然的:\(f_i(t) = g_{i-1}(t) + |t-b_i|\)。
事实上有了这个转移,离散化后可以做到 \(O(n^2)\),足以通过原题,但无法通过数据加强版。考虑优化。注意到绝对值函数有凸性,想到 Slope Trick 维护斜率。
我断言,\(\forall i\),\(f_i(t)\) 和 \(g_i(t)\) 函数均是凸函数。考虑归纳证明。
初始时,\(g_0(t)\) 为一条直线,是凸函数。假设 \(g_{i-1}\) 是凸函数。因为 \(|t-b_i|\) 则是一个典型的凸函数,两凸函数相加仍为凸函数(可以用数学方法证明,这里不展开),因此 \(f_i\) 是凸函数。因为 \(g_i(t)\) 是对 \(f_i(t)\) 取前缀 \(\min\),即将 \(f_i(t)\) 的所有斜率大于 \(0\) 的部分截掉,因此 \(g_i(t)\) 仍然是凸函数。证毕。
Slope Trick 的核心步骤是,维护一个可重集 \(S\),用于存放所有拐点。规定:在某横坐标处,斜率跳跃了多少,就在这个点放多少个拐点。因此 \(|t-b|\) 的拐点集为 \(\{b,b\}\)。
于是,一个凸函数可以由拐点可重集 \(S\) 和常数 \(C\)(用于表示函数最右侧直线的截距)来描述。
当两个凸函数相加时,\(S\) 直接合并,\(C\) 直接相加。
关注状态转移方程。设 \(g_{i-1}(t)\) 的拐点集为 \(S\),常数为 \(C\)。
对于转移方程,有如下两个操作:
-
加上 \(|t-b_i|\):
对于 \(f_i(t)\),\(S' = S \cup \{b_i,b_i\},\ C' = C\)。
-
取前缀最小值:
去除最右边的拐点(即从 \(S'\) 中取出最大的拐点 \(M\) 并删除一个),此时函数最低点会发生移动,整体常数 \(C'\) 的变化量为 \(\Delta_i = M - b_i\)。因为对于每个 \(i\),\(\Delta_i\) 都表示当前 \(g_i\) 最小值的变化量,所以 \(\sum\limits_{i=1}^n \Delta_i\) 就表示全局最小值,即为答案。
实现方面,我们可以维护一个优先队列作为拐点集。
需要再深入理解一下 Slope Trick。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 3005;
int n, ans, b;
priority_queue<int> q;
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> b; b -= i;
q.push(b); q.push(b);
int M = q.top(); q.pop();
ans += M - b;
}
cout << ans;
return 0;
}
D1 - Helping People
Description
初始给定长度为 \(n\) 的数列 \(a\),有 \(q\) 个线段,其中第 \(i\) 个线段为 \([l_i,r_i]\),有 \(p_i\) 的概率使线段覆盖到的数字全部加 \(1\)。保证所有线段两两不互相交错,即它们只能相离或包含。
求操作完成后数列最大值的期望。
\(1 \le n \le 10^5,\ 1 \le q \le 5000,\ \epsilon \le 10^{-6}\)。
Solution
首先最大值的期望不是期望的最大值。否则这题会变得非常简单。
考虑把这个期望变假。具体地,有 \(E = \sum\limits_i p_i \cdot x_i\),因此我们只需要求出概率即可。
区间不相互交错的条件让我们想到在线段上建树(不是线段树哦),从而树上 DP 出概率。具体地,我们不妨定义 \(dp_{u,i}\) 表示 \(u\) 节点对应的区间内最大值 \(\le i\) 的概率。显然可以转移,但值域太大,存不下。
考虑把值域缩紧。由于某区间最大值 \(M\) 在某次操作后一定属于 \([M,M+q]\),所以属于以上区间的 \(i\) 才能构成合法状态。因此,可以定义 \(dp_{u,i}\) 表示 \(u\) 节点对应的区间内最大值 \(\le i + M_u\) 的概率。由此,分析 \(u\) 的每个子节点 \(v\),考察它们原本的最大值,可以得到状态转移
特别地,当 \(i=0\) 时只考虑后面的部分即可。
答案为
可以用 ST 表维护最大值。
Code
注意这题树上 DP 和线段建树的代码实现,比较精妙。
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int M = 5005, N = 1e5+5;
vector<int> g[M];
int n, q, a[N];
long double ans = 0;
struct Segment {
int l, r, maxn;
double p;
friend bool operator < (Segment x, Segment y) {
if (x.l == y.l) return x.r > y.r;
return x.l < y.l;
}
} seg[M];
struct SparseTable {
int f[N][30], lg[N];
void init() {
lg[1] = 0;
for (int i = 2; i <= n; i++) {
lg[i] = lg[i/2] + 1;
}
for (int i = 1; i <= n; i++) {
f[i][0] = a[i];
}
for (int j = 1; (1<<j) <= n; j++) {
for (int i = 1; i <= n-(1<<j)+1; i++) {
f[i][j] = max(f[i][j-1], f[i+(1<<(j-1))][j-1]);
}
}
}
int query(int l, int r) {
int s = lg[r-l+1];
return max(f[l][s], f[r-(1<<s)+1][s]);
}
} st;
double dp[M][M];
void DFS(int u, int fa) {
dp[u][0] = 1.0 - seg[u].p;
for (auto v : g[u]) {
if (v == fa) continue;
DFS(v, u);
dp[u][0] *= dp[v][seg[u].maxn - seg[v].maxn];
}
for (int i = 1; i <= q; i++) {
double p0 = 1.0, p1 = 1.0;
for (auto v : g[u]) {
if (v == fa) continue;
p0 *= dp[v][min(i-seg[v].maxn+seg[u].maxn-1,q)];
p1 *= dp[v][min(i-seg[v].maxn+seg[u].maxn,q)];
}
dp[u][i] = seg[u].p * p0 + (1.0-seg[u].p) * p1;
}
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
st.init();
for (int i = 1; i <= q; i++) {
cin >> seg[i].l >> seg[i].r >> seg[i].p;
seg[i].maxn = st.query(seg[i].l, seg[i].r);
}
seg[++q] = {1, n, st.query(1, n), 0.0};
sort(seg+1, seg+1+q);
for (int i = 2; i <= q; i++) {
for (int j = i-1; j >= 1; j--) {
if (seg[j].l <= seg[i].l and seg[i].r <= seg[j].r) {
g[j].push_back(i), g[i].push_back(j);
break;
}
}
}
DFS(1, -1);
for (int i = 0; i <= q; i++) {
ans += (dp[1][i] - (bool)i * dp[1][i-1]) * (i + seg[1].maxn);
}
cout << fixed << setprecision(12) << ans;
return 0;
}
G1 - Boss, Thirsty
Description
给定一个 \(n \times m\) 的矩阵 \(a\)。在第 \(i\) 行可以选择 \([l,r]\) 区间内的数,并要求:
- 第 \(i\) 天选的数中,至少有一个与第 \(i-1\) 天选的数的纵坐标相同;
- 第 \(i\) 天选的数中,至少有一个与第 \(i-1\) 天选的数的纵坐标不同;
求所选数字的加和最大值。
多测,\(1 \le T \le 1000,\ 1 \le n,m,nm,\sum nm \le 2 \times 10^5,\ -10^9\le a_{i,j} \le 10^9\)。
Solution
朴素 DP 是容易的,但是空间会爆。考虑定义 \(dp_{i,j,0/1}\) 表示前 \(i\) 天,第 \(i\) 天选择的左 / 右端点为 \(j\) 的最大收益,可以覆盖到所有区间。
设第 \(i\) 天选择区间为 \([l,r]\),第 \(i-1\) 天选择区间为 \([l',r']\),考虑分情况讨论转移:
-
\(l < l'\)
如图所示:
l' r' --------------- l r ==============考虑固定 \(l\) 枚举 \(l'\),则 \(l'\) 必须从 \(l+1\) 开始;\(r\) 则为固定左端点 \(l\),右端点 \(\gt l'\) 的区间中加和最大的区间右端点,可以前缀和预处理,具体地,我们记 \(V(l,l')\) 表示上述变量,对每一行做前缀和,则有
\[V(l,l') = \max_{j=l'}^m(s_j-s_{l-1}) \]则状态转移方程为
\[dp_{i,l,0} = \max_{l'=l+1}^m(dp_{i-1,l',0} + V(l,l')) \] -
\(l = l'\)
如图所示:
l' r' ------------- l r =======================如果固定 \(l\) 枚举 \(l'\) 的话,无法确定合法的右端点位置,因此需要从右端点转移,即
\[dp_{i,l,0} = \max_{r'=l}^m(dp_{i-1,r',1} + V(l,r'+1)) \] -
\(r > r'\)
-
\(r = r'\)
这两种情况同型,不予列出。
考虑优化。拿第一种情况举例,可以将 \(\max\limits_{j=l'}^m(s_j-s_{l-1})\) 变形为 \(\max\limits_{j=l'}^m(s_j)-s_{l-1}\),扫一遍求前缀和的后缀最大值即可。状态转移方程同理,也可以求出后缀最大值然后再转移。余下三种情况同理,但后两种情况需要维护的是前缀和的后缀最小值。具体细节见代码。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
int T, n, m;
void solve() {
cin >> n >> m;
vector<vector<int>> a(n+5, vector<int>(m+5));
vector<vector<int>> dp[2];
dp[0] = dp[1] = vector<vector<int>>(n+5, vector<int>(m+5, -inf));
vector<int> s(m+5, 0);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
for (int j = 1; j <= m; j++) {
s[j] = s[j-1] + a[1][j];
}
// BORDER
int maxn = -inf;
for (int l = m; l >= 1; l--) {
maxn = max(maxn, s[l]);
dp[0][1][l] = maxn - s[l-1];
}
int minn = inf;
for (int r = 1; r <= m; r++) {
minn = min(minn, s[r-1]);
dp[1][1][r] = s[r] - minn;
}
// DP
for (int i = 2; i <= n; i++) {
s.clear();
for (int j = 1; j <= m; j++) {
s[j] = s[j-1] + a[i][j];
}
int maxn = -inf, maxs = -inf, mins = inf;
for (int l = m; l >= 1; l--) {
if (l+1 <= m) maxs = max(maxs, s[l+1]);
maxn = max(maxn, dp[0][i-1][l+1] + maxs);
dp[0][i][l] = max(dp[0][i][l], maxn - s[l-1]);
}
maxn = -inf, maxs = -inf, mins = inf;
for (int l = m; l >= 1; l--) {
if (l+1 <= m) maxs = max(maxs, s[l+1]);
maxn = max(maxn, dp[1][i-1][l] + maxs);
dp[0][i][l] = max(dp[0][i][l], maxn - s[l-1]);
}
maxn = -inf, maxs = -inf, mins = inf;
for (int r = 1; r <= m; r++) {
if (r-2 >= 0) mins = min(mins, s[r-2]);
maxn = max(maxn, dp[1][i-1][r-1] - mins);
dp[1][i][r] = max(dp[1][i][r], maxn + s[r]);
}
maxn = -inf, maxs = -inf, mins = inf;
for (int r = 1; r <= m; r++) {
if (r-2 >= 0) mins = min(mins, s[r-2]);
maxn = max(maxn, dp[0][i-1][r] - mins);
dp[1][i][r] = max(dp[1][i][r], maxn + s[r]);
}
}
int ans = -inf;
for (int i = 1; i <= m; i++) {
ans = max({ans, dp[0][n][i], dp[1][n][i]});
}
cout << ans << '\n';
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> T;
while (T--) {
solve();
}
return 0;
}

浙公网安备 33010602011771号