【做题笔记】动态规划专题(DP)
动态规划专题(DP)
这里记录笔者这几天做的有关于 dp 的题目。
树形 dp
- 洛谷 P1122
题目链接:https://www.luogu.com.cn/problem/P1122
题意:选出一个联通分量,使得联通分量的点的点权和最大。
思路:考虑以 \(f_i\) 表示以点 \(i\) 为根节点子树联通分量权值的最大值。
于是初始化有 \(f_i=a_i\),因为他的子树不管断掉几个一定包含自己。
如果不是叶子节点,考虑如果它儿子(设为 \(j\))的 \(f_j>0\),则这个节点一定对 \(f_i\) 有贡献,加上它的子树会使得答案值更大,否则不选。
于是就有了递推方程:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e4 + 5;
inline int read() {
int x(0),f(0);
char ch=getchar();
for(; !isdigit(ch); ch=getchar()) f|=(ch=='-');
for(; isdigit(ch); ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
return f?-x:x;
}
int val[N],dp[N];
int n,m,ans=numeric_limits<int>::min();
struct graph {
int to,nxt;
} G[N<<1];
int cnt,head[N];
void addEdge(int u,int v) {
G[++cnt]= {v,head[u]};
head[u]=cnt;
}
void dfs(int x,int fa) {
dp[x]=val[x];
for(int i=head[x]; i; i=G[i].nxt) {
int t=G[i].to;
if(t==fa) continue;
dfs(t,x);
if(dp[t]>0)
dp[x]+=dp[t];
}
}
int main() {
n=read();
for(int i=1; i<=n; i++) val[i]=read();
for(int i=1; i<n; i++) {
int u,v;
u=read(), v=read();
addEdge(u,v);
addEdge(v,u);
}
dfs(1,-1);
for(int i=1; i<=n; i++)
ans=max(ans,dp[i]);
printf("%d\n",ans);
}
线性 dp
- 洛谷 P8725
题目链接:https://www.luogu.com.cn/problem/P8725。
用 \(f_{i,j}\) 表示第 \(i\) 秒时用了 \(j\) 点体力的合法方案。
显然当前位置即为 \(i-2 \times j\)。如果 \(i-2 \times j \ge d\) 则显然不合法。
如果合法,考虑当前位置可能从哪几个位置转移而来:
- 上一秒体力为 \(j-1\),这一秒花了体力,即 \(f_{i-1,j-1}\)。
- 上一秒体力为 \(j\),这一秒没花,即 \(f_{i-1,j}\)。
值得一提的是,如果 \(j=0\),则第一种情况不合法,取第二种情况即可。
于是状转方程就有了:
注意取模。
#include<bits/stdc++.h>
using namespace std;
const int N = 3e3 + 5;
const int mod = 1e9 + 7;
inline int read() {
int x(0),f(0);
char ch=getchar();
for(; !isdigit(ch); ch=getchar()) f|=(ch=='-');
for(; isdigit(ch); ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
return f?-x:x;
}
int d,t,m;
int dp[N][N];
int main() {
d=read(), t=read(), m=read();
dp[0][0]=1;
for(int i=1; i<=t; i++) {
for(int j=0; j<=min(i,m); j++) {
int now = i - j * 2;
if(now >= d) continue;
if(j == 0) dp[i][j] = dp[i-1][j] % mod;
else dp[i][j] = (dp[i][j] + dp[i-1][j] + dp[i-1][j-1]) % mod;
}
}
printf("%d\n",dp[t][m]);
}
- 洛谷 P8656
我们注意到,如果 \(a_i \bmod k \neq a_j \bmod k\),那么这两种实力的人是互不影响的,也就是说,和 \(a_i\) 有关的选择和和 \(a_j\) 有关的选择毫无关系,我们考虑把 \(\{a\}\) 按照 \(\bmod k\) 的值分成 \(k\) 类。
在每一类中,我们先排序,再去重成不重复的元素,然后就是经典的线性 dp 了:设 \(f_{i,0/1}\),表示第 \(i\) 个元素结尾最多能取 \(f_{i,0/1}\) 个人。取第 \(i\) 个即为 \(f_{i,1}\),否则为 \(f_{i,0}\)。
于是状转方程就很容易推了,设 \(d_i\) 为 \(a_i\) 的出现次数,\(p\) 为 \(a_i \bmod k\) 的值:
注意当 \((a_{p,i}-a_{p,i-1}) = k\) 时只能选一种。以及 dp 数组记得每一次之前都要初始化。
记得当 \(k=0\) 的时候要特判,其最优解即为每个元素都选一个,不判的话在 C++ 中 \(\bmod 0\) 会 RE。
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
const int mod = 1e9 + 7;
inline int read() {
int x(0),f(0);
char ch=getchar();
for(; !isdigit(ch); ch=getchar()) f|=(ch=='-');
for(; isdigit(ch); ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
return f?-x:x;
}
int n,k,ans;
int a[N],d[N],g[N];
int dp[N][2];
vector<int> v[N];
int main() {
n=read(), k=read();
for(int i = 1; i <= n; i++) a[i]=read(),d[a[i]]++;;
sort(a+1, a+n+1);
int m = unique(a+1, a+n+1)-a-1;
if(k == 0) {
printf("%d\n", m);
return 0;
}
for(int i = 1; i <= m; i++) {
g[a[i]]++;
if(g[a[i]] == 1)
v[a[i] % k].push_back(i);
}
// dp
for(int i = 0; i < k; i++) {
if(v[i].empty()) continue;
int siz = v[i].size();
memset(dp, 0x3f, (siz + 5) * sizeof(dp[0][0]));
for(int j = 0; j < siz; j++) {
if(j == 0) dp[0][1] = d[a[v[i][j]]], dp[0][0] = 0;
else if((a[v[i][j]] - a[v[i][j-1]]) == k) {
dp[j][0] = max(dp[j-1][1], dp[j-1][0]);
dp[j][1] = dp[j-1][0] + d[a[v[i][j]]];
} else {
dp[j][0] = max(dp[j-1][1], dp[j-1][0]);
dp[j][1] = max(dp[j-1][1], dp[j-1][0]) + d[a[v[i][j]]];
}
}
ans += max({dp[siz - 1][0], dp[siz - 1][1]});
}
printf("%d\n",ans);
}
- 洛谷 P8786
闲话:\(10^9+7=\)
0x3b9aca07
状态设计:设 \(f_{i,j,k}\) 为第 \(i\) 秒,看了 \(j\) 次花,酒量为 \(k\) 时,合法的可能数。
首先我们要明确一个范围:酒显内酒的最大值是多少?显然不会超过 \(100\),不然即使全是花也不能喝空。
我们不从它前面的转移到它,从它转移到它后面的:
如果 \(f_{i,j,k} \neq 0\),那么当前有合法的可能数,于是:
- 如果 \(k > 0\),即酒显没空,则它可以转移到 \(f_{i+1,j+1,k-1}\),即这一秒看了花。
- 如果 \(k < 50\),即酒显没空,且加酒后不超过 \(100\),则它可以转移到 \(f_{i+1,j,k \times 2}\),即这一秒加了酒。
注意 \(j\) 从 \(0\) 到 \(m-1\),因为最后一次要留给花。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e2 + 5;
const int mod = 0x3b9aca07;
inline int read() {
int x(0),f(0);
char ch=getchar();
for(; !isdigit(ch); ch=getchar()) f|=(ch=='-');
for(; isdigit(ch); ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
return f?-x:x;
}
int n,m;
int ans,dp[N + N][N][N]; // time i, flower met j, wine left k
inline void getmod(int &x){
x %= mod;
}
int main() {
n=read(), m=read();
dp[0][0][2] = 1;
for(int i = 0; i <= n + m; i++) {
for(int j = 0; j < m; j++) {
for(int k = 1; k <= m; k++) {
if(dp[i][j][k]){
if(k > 0) getmod(dp[i + 1][j + 1][k - 1] += dp[i][j][k]);
if(k <= 50) getmod(dp[i + 1][j][k * 2] += dp[i][j][k]);
}
}
}
}
printf("%d\n", dp[n + m][m][0]);
}
状压 dp
- 洛谷 P8687
闲话:最优解 rk1 是用 IDA* 做的,每个点都是 4ms,恐怖如斯。
对于 \(1 \le n \le 100,1 \le m \le 20\),想到状压 dp。
设计状态:用一个整数的二进制表示,第 \(i\) 位为 \(1\) 则有这种类型的糖果,反之没有。
用 \(\Theta(n \times 2^m)\) 的时间枚举每一个店铺,把能更新的就更新掉,即
其中 \(\text{state}\) 表示已经有的状态,这个可以从 \(1 \sim 2^m\) 枚举。
时间复杂度是 \(\Theta(n \times 2^m)\) 的,可以卡过去。
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
inline int read() {
int x(0), f(0);
char ch = getchar();
for(; !isdigit(ch); ch=getchar()) f |= (ch == '-');
for(; isdigit(ch); ch=getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return f ? -x : x;
}
int n,m,k,z;
int dp[1<<N],a[N];
int main() {
memset(dp, 0x3f, sizeof dp);
n=read(), m=read(), k=read();
for(int i = 1; i <= n; i++) {
int x = 0, y;
for(int i = 1; i <= k; i++) {
y = read() - 1;
x |= (1 << y);
}
dp[x] = 1;
a[i] = x;
z |= x;
}
if(z + 1 != (1 << m)) return puts("-1") & 0;
int ed = (1 << m) - 1;
for(int i = 1; i <= n; i++){
for(int state = 0; state <= ed; state++){
if(dp[state] > 200) continue;
int to = a[i] | state;
dp[to] = min(dp[to], dp[state] + 1);
}
}
printf("%d\n", dp[ed]);
return 0;
}
- 洛谷 P1433
待填坑。