【做题笔记】动态规划专题(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\) 有贡献,加上它的子树会使得答案值更大,否则不选。

于是就有了递推方程:

\[f_i=\sum_{j \in \text{son}(i)} f_j \qquad \text{If } f_j>0 \]

#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\) 则显然不合法。

如果合法,考虑当前位置可能从哪几个位置转移而来:

  1. 上一秒体力为 \(j-1\),这一秒花了体力,即 \(f_{i-1,j-1}\)
  2. 上一秒体力为 \(j\),这一秒没花,即 \(f_{i-1,j}\)

值得一提的是,如果 \(j=0\),则第一种情况不合法,取第二种情况即可。

于是状转方程就有了:

\[f_{i,j}=\begin{cases}0 & \text{If }i-2 \times j \ge d \\ f_{i-1,j} & \text{If } j=0 \\ f_{i-1,j-1}+f_{i-1,j} & \text{Otherwise}\end{cases} \]

注意取模。

#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\) 的值:

\[f_{i,1}=\begin{cases}d_i & \text{If } i=0 \\ f_{i-1,0}+d_i & \text{If } (a_{p,i}-a_{p,i-1}) = k \\ \max(f_{i-1,0},f_{i-1,1})+d_i & \text{Otherwise.}\end{cases} \]

\[f_{i,0}=\begin{cases}0 & \text{If } i=0 \\ \max(f_{i-1,1},f_{i-1,0}) & \text{Otherwise}\end{cases} \]

注意当 \((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\),那么当前有合法的可能数,于是:

  1. 如果 \(k > 0\),即酒显没空,则它可以转移到 \(f_{i+1,j+1,k-1}\),即这一秒看了花。
  2. 如果 \(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)\) 的时间枚举每一个店铺,把能更新的就更新掉,即

\[f_{a_i\ | \ \text{state}}= \min(f_{a_i\ | \ \text{state}},f_{a_i}+1) \]

其中 \(\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

闲话:关于搜索卡时,他死了。

待填坑。

posted @ 2023-01-11 12:59  TheSky233  阅读(127)  评论(0)    收藏  举报