标签:typedef int 笔记 leq edge 权值 动态 规划 define
基础
换根DP
- 先考虑以 1 为根做一次 dfs,自底向下,儿子节点信息更新父亲
v -> u
- 再以 1 为根做一次dfs,在递归前通过父节点信息更新儿子节点信息
u -> v
- 考虑换根过程,根从 1 换到其他点特殊考虑,其他点互相换根时,设儿子为
x
,根为y
- 当要换根到
x
时,先减去x
在y
中的贡献,然后重新计算y
作为子树对新根x
的贡献。
- 当要换根到
应用:
- 计算树中点到其他点距离和
- 树中每个点流大小
- 每个点的最长路径
- 最大子链和(链权值为点权和, 换根DP)
换根DP计算树中每个点的最长路径
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N << 1], ne[N << 1], idx, n;
int f[N][2][2], g[N]; // f[i][0][0/1] i点最长路径长度(0)节点(1) f[i][1][0/1] 代表次长路径
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs1(int u, int p){
for(int i = h[u]; ~i; i = ne[i]){
int v = e[i];
if(v == p) continue;
dfs1(v, u);
int len = f[v][0][0] + 1;
if(len > f[u][0][0])
f[u][1][0] = f[u][0][0], f[u][1][1] = f[u][0][1], f[u][0][0] = len, f[u][0][1] = v;
else if(len > f[u][1][0])
f[u][1][0] = len, f[u][1][1] = v;
}
}
void dfs2(int u, int p){
for(int i = h[u]; ~i; i = ne[i]){
int v = e[i];
if(v == p) continue;
if(f[u][0][1] == v){
g[v] = max(f[u][1][0], g[u]) + 1;
}
else{
g[v] = max(f[u][0][0], g[u]) + 1;
}
dfs2(v, u);
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
memset(h, -1, sizeof h);
cin >> n;
for(int i = 1; i < n; i++){
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
dfs1(1, -1);
dfs2(1, -1);
for(int i = 1; i <= n; i++){
cout << f[i][0][0] + f[i][1][0] + g[i] - min(min(f[i][0][0], f[i][1][0]), g[i]) << endl;
}
return 0;
}
树上背包
问题0:
给一个 \(n(1\leq n \leq 100000)\) 个点有根树,每个点一个权值 \(v_i\) , 权值可能是负数, 求对于每个点回答它的子树中选择一个包含这个点的连通块, 最大权值和
状态: \(f[i]\) 表示以 i 为根子树连通块最大值, f[u] = a[u] + \Sigma max(0, f[v])
问题1:
给一个 \(n(1\leq n \leq 2000)\) 个点有根树,每个点一个权值 \(v_i\) , 权值可能是负数, 求对于每个点回答它的子树中选择一个包含这个点的大小为 \(m\) 的连通块, 最大权值
状态: \(f[i][j]\) 表示以 i 为根子树连通块大小为 j 的最大值, f[u][i + j] = f[u][i] + f[v][j]
实现复杂度 \(O(n^2)\) , 每对点仅一次贡献
#include<bits/stdc++.h>
using namespace std;
const int N = 2010, INF = 2e9;
int sz[N], f[N][N], n, q, a[N];
vector<int> edge[N];
// f[i][j] 表示以i为根连通块大小为 j 的最大权值
void dfs(int u){
static int tmp[N];
for(auto v: edge[u]){
dfs(v);
for(int i = 0; i <= sz[u] + sz[v]; i++)
tmp[i] = - INF; // tmp 充当滚动数组备份的作用
for(int i = 0; i <= sz[u]; i++) // 背包合并,每对点对复杂度产生1贡献,总复杂度为 O(n^2)
for(int j = 0; j <= sz[v]; j++)
tmp[i + j] = max(tmp[i + j], f[u][i] + f[v][j]);
for(int i = 0; i <= sz[u] + sz[v]; i++) // 做完这个子树背包,赋值
f[u][i] = tmp[i];
sz[u] += sz[v];
}
sz[u] += 1;
for(int i = sz[u]; i; i --) // 需要选上u点,倒序平移更新
f[u][i] = f[u][i - 1] + a[u];
f[u][0] = 0;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> q;
for(int i = 2; i <= n; i++){
int x;
cin >> x;
edge[x].pb(i);
}
for(int i = 1; i <= n; i++)
cin >> a[i];
dfs(1);
while(q--){
int u, m;
cin >> u >> m;
cout << f[u][m] << endl;
}
return 0;
}
问题2
给 \(n(1\leq n \leq 50000)\) 个点的有根树,每个点有一个权值, 回答每个点在它子树中选择一个大小为 \(m(1\leq m\leq 100)\) 包含该点的连通块最大权值和
状态: 与上述一致, 但背包大小第二维修改为 \(M(M\leq100\) , 代码几乎相同,修改一下限制即可
#include<bits/stdc++.h>
using namespace std;
const int N = 50010, INF = 2e9, M = 100;
int sz[N], f[N][M + 10], n, q, a[N];
vector<int> edge[N];
// f[i][j] 表示以i为根连通块大小为 j 的最大权值
void dfs(int u){
static int tmp[N];
for(auto v: edge[u]){
dfs(v);
for(int i = 0; i <= sz[u] + sz[v]; i++)
tmp[i] = - INF; // tmp 充当滚动数组备份的作用
for(int i = 0; i <= min(sz[u], M); i++) // 背包合并,每对点对复杂度产生1贡献,总复杂度为 O(n^2)
for(int j = 0; j <= min(sz[v], M) && i + j <= M; j++)
tmp[i + j] = max(tmp[i + j], f[u][i] + f[v][j]);
for(int i = 0; i <= sz[u] + sz[v] && i <= M; i++) // 做完这个子树背包,赋值
f[u][i] = tmp[i];
sz[u] += sz[v];
}
sz[u] += 1;
for(int i = min(sz[u], M); i; i --) // 需要选上u点,倒序平移更新
f[u][i] = f[u][i - 1] + a[u];
f[u][0] = 0;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> q;
for(int i = 2; i <= n; i++){
int x;
cin >> x;
edge[x].pb(i);
}
for(int i = 1; i <= n; i++)
cin >> a[i];
dfs(1);
while(q--){
int u, m;
cin >> u >> m;
cout << f[u][m] << endl;
}
return 0;
},
问题3
给了 \(n(1\leq n \leq 1000)\) 个点有根树, 每个点有重量 \(w_i\) 和权值 \(v_i\) , 选择一个重量和恰好为 \(m(1\leq m \leq 10000)\) 的包含根连通块最大权值和
思路:
- 在 DFS 序上做DP, 将问题转化为 线性DP, 预处理每个点在 dfs序上不选该点子树的下一个位置即
out[i] + 1
- 状态: \(f[i][j]\) 表示 DFS 序上 \([i, n]\) 中的点选择重量和恰好为 \(j\) 的最大权值,
- \(f[i][j] = max(f[r_i][j], f[i + 1][j - v[i]] + w[i])\)
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 1010, M = 1e4 + 10, INF = 2e9;
int f[N][M], n, m, tot;
int w[N], v[N], in[N], out[N], seq[N];
vector<int> edge[N];
void dfs(int u){
in[u] = ++ tot;
seq[tot] = u;
for(auto v : edge[u])
dfs(v);
out[u] = tot;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 2; i <= n; i++){
int x;
cin >> x;
edge[x].pb(i);
}
dfs(1);
for(int i = 1; i <= n; i++)
cin >> w[i];
for(int i = 1; i <= n; i++)
cin >> v[i];
for(int i = 1; i <= m; i++) f[n + 1][i] = -INF; // f[n + 1][0] = 0,其他状态不合法
for(int i = n; i >= 1; i--){
int u = seq[i];
for(int j = 0; j <= m; j++){
f[i][j] = f[out[u] + 1][j];
if(j >= v[u])
f[i][j] = max(f[i][j], f[i + 1][j - v[u]] + w[u]);
}
}
for(int k = 0; k <= m; k++)
if(f[1][k] >= 0)
cout << f[1][k] << endl;
else
cout << 0 << endl;
return 0;
}
小结
- 如果合并两个子树代价等于子树大小乘积, 复杂度为 \(O(n^2)\)
- 如果合并两个子树代价等于子树大小和 \(m\) 取最小值乘积, 复杂度为 \(O(nm)\)
- 如果不是上述两种情况, 比如合并代价为 \((size_u + size_v)^2\) , 复杂度类似 \(O(n^3)\)
- 如果是连通块间距离大小限制, 距离大小不超过子树大小, 有类似复杂度,
f[i][j]
表示以 i 为根, 距离限制为 j 的方案
树上路径
问题1:
给了一个 \(n(1\leq n \leq 2000)\) 个点的树, 给了 \(m(1\leq m \leq 2000)\) 条树上简单路径, 每个路径有一个权值, 选择一些路径, 使得每个点最多在一条路径上, 路径的权值和最大
概率期望DP
求概率起点到终点推导,求期望终点向起点推导
例:
利用高斯消元消除后效性(有环)
题意
蜗蜗的世界里有 \(n\) 个城市,城市之间通过 \(m\) 条单向高速公路连接,初始他在 1 号城市。蜗蜗想去 \(n\) 号城市游玩,假设现在他在
\(x\) 号城市,他会等概率地选择从 \(x\) 出发的高速公路中的一条走过去。如果没有任何从 \(x\) 号城市出发的高速公路,他就只能留在原地了。蜗蜗会一直走直到他走到
\(n\) 号城市。请问蜗蜗期望经过多少条高速公路能够走到 \(n\) 号城市。
数据范围
\(2\leq n\leq 100\)
\(1\leq m\leq 1000\)
思路
- 设计状态:
f[i]
表示从 i 到 n 期望经过的边数 - 状态转移: \(f[i] = \Sigma \frac{f[j] + 1}{d[i]} =\Sigma \frac{f[j]}{d[i]} + 1\) , j 是 i 能通向的边终点
- 对于每一个点产生的式子相当于一个方程组的一个方程, n 个点对应 n 个方程组, 对于每个 f[i] 一定有解, 常数项作为增广矩阵最右元素列
- 答案对质数取模, 要用快速幂求逆元
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 110, p = 1e9 + 7;
ll f[N][N], n, m;
vector<int> edge[N];
ll qmi(ll a, ll k, int mod){
ll res = 1;
while(k){
if(k & 1)
res = res * a % mod;
a = a * a % mod;
k >>= 1;
}
return res;
}
void gauss(){
int c, r;
for(c = 1, r = 1; c <= n; c ++){
int t = r - 1;
for(int i = r; i <= n; i++)
if(f[i][c]){
t = i;
break;
}
if(t == r - 1) continue;
for(int j = c; j <= n + 1; j++)
swap(f[r][j], f[t][j]);
for(int i = n + 1; i >= c; i--)
f[r][i] = f[r][i] * qmi(f[r][c], p - 2, p) % p;
for(int i = r + 1; i <= n; i++){
if(f[i][c])
for(int j = n + 1; j >= c; j--){
f[i][j] -= 1ll * f[i][c] * f[r][j] % p;
if(f[i][j] < 0)
f[i][j] += p;
}
}
r++;
}
for(int i = n; i >= 1; i--){
for(int j = i + 1; j <= n; j++){
f[i][n + 1] -= f[j][n + 1] * f[i][j] % p;
if(f[i][n + 1] < 0)
f[i][n + 1] += p;
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
while(m--){
int a, b;
cin >> a >> b;
edge[a].pb(b);
}
for(int i = 1; i < n; i++){
f[i][i] = 1, f[i][n + 1] = 1;
int sz = edge[i].size();
for(auto v: edge[i]){
f[i][v] = p - qmi(sz, p - 2, p);
}
}
f[n][n] = 1;
gauss();
cout << f[1][n + 1] << endl;
return 0;
}
标签:typedef,int,笔记,leq,edge,权值,动态,规划,define 来源: https://www.cnblogs.com/Roshin/p/DP_notes.html
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。