AT_dp_[ ]

题单跳转

1. 无聊的统计

难度统计

难度 数量 题目
4 A,B,D,H
7 C,E,F,G,K,L,N
绿 6 I,J,M,P,Q,R
7 O,S,T,U,W,X,Y
2 V,Z

该博客的 Markdown 源码共有 \(\approx 1.03\times 10^4\) 字符(截至最后一次编辑),673 行。

2. 正文

橙 - A,B,D,H

A

\(f_i\) 表示到第 \(i\) 个石头的费用。

则易得转移方程 \(f_i=\min\!\left\{f_{i-1}+|h_{i-1}-h_i|,f_{i-2}+|h_{i-2}-h_i|\right\}\)

初始化 \(f_0=\inf\)

B

与 A 类似,但不同的是,我们需要枚举前 \(k\) 个石头。

此时变为 \(f_i=\min\limits_{1\le j\le k}\!\left\{f_{i-j}+|h_{i-j}-h_i|\right\}\)

注意不要访问到负数下标。

D

01 背包板子。

\(f_{i,j}\) 表示只有前 \(i\) 个物品且容量为 \(j\) 时的最大价值。

不选时,\(f_{i,j}=f_{i-1,j}\)
选时,\(f_{i,j}=f_{i-1,j-w_i}+v_i\)

两者取 \(\min\) 即可。

H

过河卒加强(多个障碍)+弱化(不用向八个方向扩散)版。

\(f_{i,j}\) 为走到 \((i,j)\) 时的方案数。

\(f_{i,j}=\begin{cases}0 & (i,j)\,\text{为障碍} \\ f_{i-1,j}+f_{i,j-1} & (i,j)\,\text{不是障碍}\end{cases}\)

黄 - C,E,F,G,K,L,N

C

这不是橙题吗。

\(f_{i,j}\) 为前 \(i\) 天中最后一天选择 \(j\) 活动时的快乐值。

\(f_{i,1}=f_{i-1,2}+f_{i-1,3}\),其他同理。

答案取 \(\max\left\{f_{n,1},f_{n,2},f_{n,3}\right\}\)

E

01 背包,但是 \(W\le 10^9\),用 \(O(nW)\) 会爆。

注意到 \(v_i\le 10^3\),我们可以反向思考,设 \(f_{i,j}\) 表示只有前 \(i\) 个物品且 总价值\(j\) 时的 最小容量

不选时 \(f_{i,j}=f_{i-1,j}\),选时 \(f_{i,j}=f_{i-1,j-v_i}+w_i\)

最后选总价值最大且容量 \(\le W\) 的值。

时间复杂度 \(O(n^2V)\),其中 \(V=\max v_i\)

F

\(f_{i,j}\)\(a\) 的前 \(i\) 个字符与 \(b\) 的前 \(j\) 个字符的 LCS 长度。

\(f_{i,j}=\begin{cases}0 & i\le0\text{ or } j\le0\\f_{i-1,j-1}+1 & a_i=b_j \\ \max\{f_{i,j-1},f_{i-1,j}\} & a_i\not=b_j\end{cases}\)

但题目要求输出的是答案而非长度。

我们可以记录一个数组 pre[i][j],表示 \(f_{i,j}\) 在递推时用的是哪个位置的数。

在输出时,我们就可以后序遍历,从右下角通过 pre 数组的指向走到左上角,并在 \(a_i=b_j\) 时及时输出。

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;

int n,m;
string s,t;
int dp[3140][3140],pre[3140][3140];

void opt(int p,int q){
	if(p==0&&q==0){
		return;
	}
	if(pre[p][q]==1){
		opt(p-1,q-1);
		cout<<s[p];
	}
	if(pre[p][q]==2) opt(p,q-1);
	if(pre[p][q]==3) opt(p-1,q);
}

int main(){
	cin>>s>>t;
	n=s.length(); m=t.length(); s="#"+s; t="$"+t;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(s[i]==t[j]){
				dp[i][j]=dp[i-1][j-1]+1;
				pre[i][j]=1;
			}else{
				if(dp[i-1][j]<dp[i][j-1]){
					dp[i][j]=dp[i][j-1];
					pre[i][j]=2;
				}else{
					dp[i][j]=dp[i-1][j];
					pre[i][j]=3;
				}
			}
		}
	}
	if(dp[n][m]!=0) opt(n,m);
	return 0;
} 

G

DAG 最长路。

\(f_i\) 为终点为 \(i\) 时的最长路,则我们可以遍历指向它的所有结点,取 \(\max\) 后 +1。

由于顺序不固定,需要按拓补序遍历。

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n,m;
vector<int> g[114514],rg[114514];
int dp[114514],in[114514],in2[114514];
int st[114514],cnt=0;
queue<int> q;
bool vst[114514];

int main(){
	ios::sync_with_stdio(false); cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v; cin>>u>>v;
		g[u].push_back(v);
		rg[v].push_back(u);
		in[v]++; in2[v]++;
	}
	for(int i=1;i<=n;i++){
		if(in[i]==0){
			q.push(i);
			vst[i]=1;
		}
	}
	while(!q.empty()){
		int f=q.front(); q.pop();
		st[++cnt]=f;
		for(int i=0;i<g[f].size();i++){
			int v=g[f][i];
			in[v]--;
			if(in[v]==0&&!vst[v]){
				q.push(v);
				vst[v]=1;
			}
		}
	}
	for(int i=1;i<=n;i++){
		int u=st[i];
		if(in2[u]==0) continue;
		for(int j=0;j<rg[u].size();j++){
			dp[u]=max(dp[u],dp[rg[u][j]]);
		}
		dp[u]++;
	}
	int ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
	cout<<ans<<endl;
	return 0;
}

K

\(f_{i}\) 为有 \(k\) 个石子时的必胜情况,\(0\) 为先手必败,\(1\) 为先手必胜。

可以发现,如果先手能转移到一个先手必败的局面,则这时的局面就是先手必胜的。

于是可得 \(f_i=f_i\operatorname{or}(\operatorname{not}f_{i-a_j})\;(1\le j\le n)\)

L

\(f_{i,j}\) 为在 \([i,j]\) 区间中的必胜情况,数字越大,对先手越利,反之亦然。

为了方便解答,可将这个值设为题目中的 \(X-Y\)

此时,先手会尽力让这个值变大,后手会尽力让这个值变小。

此时转移方程就是:

\[f_{i,j}=\begin{cases} \max\{f_{i+1,j}+a_i,f_{i,j-1}+a_j\} & 先手 \\ \min\{f_{i+1,j}-a_i,f_{i,j-1}-a_j\} & 后手 \end{cases}\]

此时用记忆化搜索更简单。

珍爱生命,远离 DP。

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n;
int a[3140];
ll f[3140][3140];

ll dfs(int i,int j,bool isf){
	if(i>j) return 0;
	if(f[i][j]) return f[i][j];
	dfs(i+1,j,!isf);
	dfs(i,j-1,!isf);
	if(isf){
		f[i][j]=max(f[i+1][j]+a[i],f[i][j-1]+a[j]);
	}else{
		f[i][j]=min(f[i+1][j]-a[i],f[i][j-1]-a[j]);
	}
	return f[i][j];
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	cout<<dfs(1,n,1)<<endl;
	return 0;
}

N

经典的区间 dp 模板题。

\(f_{i,j}\) 为合并 \([i,j]\) 区间内的数的最小代价。

我们可以枚举一个点 \(k\in [i,j)\),表示将 \([i,k]\)\((k,j]\) 的石子合并。

\(f_{i,j}=\min\limits_{i\le k<j}\{f_{i,k}+f_{k+1,j}+\sum\limits_{i\le p\le j}\!\!\!a_p\}\)

注意 \(i\) 要倒序枚举,答案为 \(f_{1,n}\)

绿 - I,J,M,P,Q,R

I

\(f_{i,j}\) 为共有 \(i+j\) 个硬币时投出 \(i\) 个正面和 \(j\) 个反面的概率。

因为每次抛硬币是独立的,所以可以从前面推过来:

\[f_{i,j}=\color{blue}p_{i,j}f_{i-1,j}\color{black}+\color{red}(1-p_{i,j})f_{i,j-1} \]

其中,蓝色部分表示抛出正面的情况,红色部分表示抛出反面的情况。

因为总共有 \(n\) 枚硬币,所以可以枚举正面的数量 \(i\),并对 \(dp_{i,n-i}\) 求和(其中 \(i>n-i\))。

在递推时注意不要访问到负数下标。

J

与 I 类似,可以设 \(f_{i,j,k}\) 为剩下 \(i\) 个单个寿司,\(j\) 个双拼和 \(k\) 个三拼时的概率。

\(s\)\(i+j+k\),则:

  • 选到空盘的概率为 \(\dfrac{n}{s}\)

  • 选到单盘的概率为 \(f_{i-1,j,k}\cdot\dfrac{i}{s}\)

  • 选到双拼的概率为 \(f_{i+1,j-1,k}\cdot\dfrac{j}{s}\)

  • 选到三拼的概率为 \(f_{i,j+1,k-1}\cdot\dfrac{k}{s}\)

以上四个相加即可。结果为 \(f_{\text{cnt}_1,\text{cnt}_2,\text{cnt}_3}\),其中 \(\text{cnt}_i\) 为开始时有 \(i\) 个寿司的盘子的数量。

记得枚举时跳过 \(f_{0,0,0}\)

M

\(f_{i,j}\) 为前 \(i\) 个人分 \(j\) 个糖的方案数。

\(f_{i,j}=\sum\limits_{k=j-a_i}^j f_{i-1,k}\)

但我们显然是不能暴力枚举的。

注意到每次求和都是一个区间,考虑线段树前缀和,时间复杂度降为 \(O(nk)\)

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;

int n,p;
ll a[114];
ll dp[114][114514];
ll pre[114514];

ll query(int l,int r){
	return l==0?pre[r]%mod:(pre[r]+mod-pre[l-1])%mod;
}

int main(){
	cin>>n>>p;
	for(int i=1;i<=n;i++) cin>>a[i];
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		pre[0]=dp[i-1][0];
		for(int j=1;j<=p;j++) pre[j]=(pre[j-1]+dp[i-1][j])%mod;
		for(int j=0;j<=p;j++) dp[i][j]=query(max(0ll,j-a[i]),j);
	}
	cout<<dp[n][p]<<endl;
	return 0;
}

P

树型 DP 基本题。

\(f_{i,c}\) 为当第 \(i\) 个点的父节点颜色为 \(c\) 时,这个节点的子树的染色方案。(这里可以设 \(1\) 为根,但如果要设为 \(n\)\(\left\lfloor\frac{n}{2}\right\rfloor\) 甚至 rand()%n+1 也不是不行。)

然后就可以得出转移方程(\(F\) 为父节点,\(S\) 为所有子节点):

\[f_{i,c}=\begin{cases} \prod\limits_{j}^{S} f_{j,0}+\prod\limits_{j}^{S} f_{j,1} & c=0 \text{ and } S\neq\text\O \\ \prod\limits_{j}^{S} f_{j,0} & c=1 \text{ and } S\neq\text\O \\ 2 & S=\text{\O}\text{ and } c=0 \\ 1 & S=\text{\O}\text{ and } c=1 \end{cases} \]

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;

int n;
vector<int> g[114514];
ll mem[114514][2];

ll dfs(int u,int fa,int fc){
	if(mem[u][fc]) return mem[u][fc];
	if(g[u].size()==1&&fa!=0){
		return (fc==1?1:2);
	}
	ll ans=0,tmp1=1,tmp2=1;
	for(int i=0;i<g[u].size();i++){
		int v=g[u][i];
		if(v==fa) continue;
		tmp1=(tmp1%mod)*(dfs(v,u,0)%mod)%mod;
	}
	ans+=tmp1;
	if(fc==0){
		for(int i=0;i<g[u].size();i++){
			int v=g[u][i];
			if(v==fa) continue;
			tmp2=(tmp2%mod)*(dfs(v,u,1)%mod)%mod;
		}
		ans+=tmp2;
	}
	mem[u][fc]=ans;
	return ans;
}

int main(){
	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);
	}
	cout<<dfs(1,0,0)%mod<<endl;
	return 0;
} 

Q

\(f_i\) 为前 \(i\) 个元素的最大和,则转移方程为 \(f_i=\max\limits_{1\le j<i,h_j\le j_i}\{f_j\}+a_i\)

然而如果直接枚举时间复杂度是 \(O(n^2)\) 的,无法通过。

注意到高度各不相同且 \(\le 2\times 10^5\),我们可以使用一个树状数组来维护这个最大值。

添加时可以将高度作为下标,\(f_i\) 作为权值。

反正只要访问从 1 开始的区间谁管能不能差分呢。

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

ll n;
ll a[202400],h[202400];
ll pmx[202400];
ll dp[202400];

inline ll lb(ll x){return x&(-x);}

void add(ll k,ll p){
	while(p<=n){
		pmx[p]=max(pmx[p],k);
		p+=lb(p);
	}
}

ll query(ll p){
	ll tmp=0;
	while(p>0){
		tmp=max(tmp,pmx[p]);
		p-=lb(p);
	}
	return tmp;
}

int main(){
	cin>>n;
	for(ll i=1;i<=n;i++) cin>>h[i];
	for(ll i=1;i<=n;i++) cin>>a[i];
	for(ll i=1;i<=n;i++){
		dp[i]=query(h[i])+a[i];
		add(dp[i],h[i]);
	}
	ll ans=0;
	for(ll i=1;i<=n;i++) ans=max(ans,dp[i]);
	cout<<ans<<endl;
	return 0;
}

R

对于这道题,我们设 \(f^t_{i,j}\) 表示起点为 \(i\),终点为 \(j\),且长度为 \(t\) 的路径条数。

由乘法原理可得 \(f_{i,j}^t=\sum\limits_{k=1}^{n}f_{i,k}^{t-1}\cdot a_{k,j}\)

注意到这个式子很像就是矩阵乘法,所以可以把递推过程换成矩阵幂。

也就是说,\(f^t=\bm{A}^t\),其中 \(\bm{A}\) 为初始的邻接矩阵。

答案就是 \(\bm{A}^k\) 的各个元素之和,由于 \(k\le 10^{18}\),要用矩阵快速幂加速,时间复杂度 \(O(n^3\log k)\)

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;

struct mtr{
	ll a[114][114];
	int n;
	mtr(){memset(a,0,sizeof(a));}
	mtr operator*(mtr b){
		mtr c;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				for(int k=1;k<=n;k++){
					c.a[i][j]+=a[i][k]*b.a[k][j];
					c.a[i][j]%=mod;
				}
			}
		}
		c.n=n;
		return c;
	}
};

ll n,k;
mtr a;

mtr pow(mtr a,ll k){
	mtr res; res.n=n; for(int i=1;i<=n;i++) res.a[i][i]=1;
	while(k){
		if(k%2==1) res=res*a;
		a=a*a;
		k/=2;
	}
	return res;
}

int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++) cin>>a.a[i][j];
	}
	a.n=n;
	mtr res=pow(a,k);
	ll ans=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			ans+=res.a[i][j];
			ans%=mod;
		}
	}
	cout<<ans%mod<<endl;
	return 0;
}

蓝 - O,S,T,U,W,X,Y

O

注意到 \(n\le 21\),考虑状压。

\(f_i\) 表示在 \(i\) 状态下的配对方案数,其中第 \(j\) 个二进制位为 1 表示第 \(j\) 个女孩纸找到了她的 npy,为 0 表示她还没找到。

同时,假设男生是按照 \(1\sim n\) 的顺序依次找 npy 的。

此时,可以让每个单身的女孩纸找一个 npy,设之前的状态为 A,现在的状态为 B,则 \(f_A\) 可以加到 \(f_B\) 上。
但写代码时一般采用倒推。

例如说,\((1111)_2\) 这个状态的方案数是 \((0111)_2\)\((1011)_2\)\((1101)_2\)\((1110)_2\) 其中一些的方案数之和。

但如果这个女孩纸找到的 npy 和她不和,则这个方案数就不能加上去。

最终答案为 \(f_{2^n-1}\)。注意递推时要按 popcount 的的顺序推。

code

// 版权归 @CuSO4_ 所有
// 未经许可,不许转载 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;

int n;
int a[25][25];
int dp[(1<<21)+5];

int popc(int x){
	int c=0;
	while(x){
		if(x&1) c++;
		x>>=1;
	}
	return c;
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++) for(int j=0;j<n;j++) cin>>a[i][j];
	dp[0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<(1<<n);j++){
			int p=popc(j);
			if(p!=i) continue;
			for(int k=0;k<n;k++){
				if(a[p][k]&&(j&(1<<k))){
					dp[j]=(dp[j]+dp[j&(~(1<<k))])%mod;
				}
			}
		}
	}
	cout<<dp[(1<<n)-1]<<endl;
	return 0;
}

注:

1<<n\(2^n\)
j&(1<<k)\(j\) 的右数第 \(k+1\) 个二进制位 \(\times 2^k\)
j&(~(1<<k))\(j\) 去掉右数第 \(k+1\) 个二进制位之后的结果。

S

T

U

W

X

Y

紫 - V,Z

V

Z

posted @ 2025-03-29 12:48  _CuSO4  阅读(75)  评论(0)    收藏  举报