闫氏DP分析法

yxc老师bilibili原视频地址

众所周知,OI界有一种神奇(ex)的算法,它几乎可以应用在任何题上,它的分析让许多OIer感到头皮发麻。它就是:DP

闫氏DP分析法

核心思想:从集合的角度考虑

\(\color {red}{“所有的DP问题,本质上都是有限集中的最值问题” — yxc}\)

阶段

动态规划有两个要点:状态与状态转移

那么阶段自然也应该有两个:状态表示状态计算

状态表示:化零为整

把几个具有相同点的元素合在一起考虑,成为一个状态

对于一个状态 \(F(i)\) ,考虑两个角度:

  • 1.集合\(F(i)\) 表示什么集合

由于 \(F(i)\) 表示的是一堆东西(这也是DP优于枚举的核心),我们要考虑这一堆东西的共同特征,如:所有满足某个条件的元素集合

这一点请仔细考虑,到底是大于等于,大于,小于,小于等于,等于......这些的不同会导致状态计算方式的不同

  • 2.属性\(F(i)\) 存的数与集合的关系:如 \(max,min,count,sum\)

很明显,\(F(i)\) 大多数时候是一个数,代表这个集合的某一个属性,多是最大值、最小值、数量、总和等。题目问什么,属性一般就是什么

状态计算:化整为零

先看 \(F(i)\) 表示的集合:

将其划分为若干个子集,要求不重(针对涉及加和类型的属性)和不漏

划分的依据:找最后一个不同点(这个待会会讲)

划分过后,求 \(F(i)\) 就可根据子集来求

如:当属性为 \(max\) 时,\(F(i)=max( \text{子集的max} )\)
当属性为 \(count\) 时,\(F(i)=\sum_ (\text{子集的count})\)


具体例子:

0-1背包问题

\(N\) 件物品以及一个容量为 \(V\) 的背包,每个物品只能使用一次。放入第 \(i\) 件物体的代价是 \(C_i\) ,得到的价值是 \(W_i\)。求在不超过容量的情况下获得的最大价值。

输入格式:

第1行两个正整数,分别表示 \(N\)\(V\)

接下来N行分别有 \(2\) 个正整数 \(C_i\)\(W_i\)

输出格式:

一行一个正整数,为最大价值总和。

样例
#in:
4 20
8 5
9 6
5 7
2 3
#out:
16

数据范围: \(1≤N≤100,1≤V≤10^6,1≤C_i≤10000,1≤W_i≤10000\)

解析:

根据乘法原理,总共的方案为 \(2^n\) 。在所有的方案数中选择一个价值最大的方案,属于有限集的最优问题,可以用试着DP来解。

状态表示

对于\(F(i,j)\)

集合:所有只考虑前\(i\)个物品,且总体积不超过的\(j\)的方案

属性:题目要求我们求最大价值,则其属性就是\(max\)

状态计算

首先看一下\(F(i,j)\)集合:

我们试着把这个集合分成若干个子集

找最后一个不同点

对于这个题,最后一个不同点就是最后一个物品选或不选

所以对于 \(F(i,j)\) 对应的集合可以如下划分

显然,这种划分方案不重不漏。

现在要分别求出左边和右边的最大值。

  • 对于左边的集合,由于它在 \(F(i,j)\) 内,且不包含 \(i\) ,那它其实相当于
    \(F(i-1,j)\)

  • 对于右边的集合,我们再次细分,将 \(i\) 单独拿出来,得到\(F(i-1,j-V_i)\)
    也就是说求右边的最大值:\(max(F(i-1,j-V_i)+W_i)\)

但是,右边这个集合不一定存在,所以要特判:\(j≥V_i\)

于是我们可以得到状态转移方程:

\[F(i,j)=max(\ F(i-1,j)\ ,\ F(i-1,j-V_i)+W_i\ ) \]

这就事朴素DP的分析过程了,至于压维等时空优化从状态转移方程出发

\(\color{red}{“DP的所有优化,都是对代码的恒等变形” — yxc}\)

对于01背包,方程中 \(F(i,j)\) 只与 \(F(i-1,x)\) 有关,且 \(x\leq j\)

所以第 \(i\) 维可以使用滚动数组滚掉。

还是放一下代码:

#include<bits/stdc++.h>
using namespace std;
int ci[4000],wi[4000];
int f[138800]//下标要>=c;
int main()
{
    int n,c;
    cin>>n>>c;
    int i,j;
    for(i=1;i<=n;i++)
    {
        cin>>ci[i]>>wi[i];
    }
    for(i=1;i<=n;i++)
    {
        for(j=c;j>=ci[i];j--)
        {
            f[j]=max(f[j],f[j-ci[i]]+wi[i]);//左子集就是f[j]=f[j],省略没写
        }
    }
    cout<<f[c];
    return 0;

}

完全背包问题

\(N\) 种物品以及一个容量为 \(V\) 的背包,每种物品有无限个可用。放入第 \(i\) 种物体的代价是 \(C_i\) ,得到的价值是 \(W_i\)。求在不超过容量的情况下获得的最大价值。

输入格式:

第1行两个正整数,分别表示 \(N\)\(V\)

接下来N行分别有 \(2\) 个正整数 \(C_i\)\(W_i\)

输出格式:

一行一个正整数,为最大价值总和。

解析

仍然从两个角度考虑:

设状态 \(F(i,j)\)

状态表示:

对于 \(F(i,j)\):

集合:所有只从前\(i\)个物品中选,总体积不超过\(j\)的所有方案。

属性:\(max\)

原因和01背包相似,毕竟都是背包问题

状态计算:

对于 \(F(i,j)\) 的集合:

划分子集

这里,最后一个物品可以选若干个,所以要把集合划分成若干个,分别代表不选第 \(i\) 种,选\(1\)\(i\),选两个\(i\)......

不重不漏性显然。

考虑每个子集怎么求。

第一个子集:不选第\(i\)种物品:显然,就是 \(F(i-1,j)\)

剩下的子集,我们可以试着考虑一般情况:选\(k\)个第\(i\)种物品。

那么每个方案可以再次细分为两个部分:\(k\)个第\(i\)种物品和前面 \(i-1\) 种物品总体积 \(j-kV_i\) 的方案

所以最大值就是:\(F(i,j)=max(\ F(i-1,j-kV_i)+kW_i\)

于是易得状态转移方程:

\[F(i,j)=max(\ F(i-1,j)\ ,\ F(i-1,j-V_i)+W_i\ ,\ F(i-1,j-2V_i)+2W_i\ ,\ \dots) \]

但是这个东西项数太多,我们平时写的只有两项啊?

那想想办法把它转换成两项

由上面的状态转移方程我们可以得到:

\[F(i,j-V_i)=max(\ F(i-1,j-V_i)\ ,\ F(i-1,j-2V_i)+W_i\ ,\ F(i-1,j-3V_i)+2W_i\ ,\ \dots) \]

观察得到,上面的每一项,都是下面的每一项加上一个 \(W_i\) 。类比,上面的最大值就是下面的最大值加上一个 \(W_i\)

于是我们可以得到我们最常用的状态转移方程:

\[F(i,j)=max(\ F(i-1,j)\ ,\ F(i,j-V_i)+W_i\ ) \]

得到了状态转移方程,我们就要来想想能否有时空优化了。

对比一下01背包和完全背包的状态转移方程

01:\(F(i,j)=max(\ F(i-1,j)\ ,\ F(i-1,j-V_i)+W_i\ )\)
完全:\(F(i,j)=max(\ F(i-1,j)\ ,\ F(i,j-V_i)+W_i\ )\)

只有一个\(i-1\)\(i\)的不同,但就是这一个不同,致使在滚动数组时一个是从大到小枚举一个是从小到大枚举。

放一下参考code:

#include <bits/stdc++.h>
using namespace std;
int ci[10010],wi[10010];
int f[100010];
int main()
{
	int t,n;
	scanf("%d%d",&t,&n);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&ci[i],&wi[i]);
	for(int i=1;i<=n;i++)
	{
		for(int j=ci[i];j<=t;j++)
		{
			f[j]=max(f[j],f[j-ci[i]]+wi[i]);
		}
	}
	cout<<f[t];
	return 0;
}

石子合并

设有\(N\)堆石子排成一排,其编号为 \(1,2,3,\dots ,N\)

每堆石子都有一定的质量,可以用一个整数来描述,现在要把这 \(N\) 堆石子合并成一堆。

每次只能合并相邻的两堆,合并的代价是这两堆石子的质量之和,合并后与这两堆石子相邻的石子将要和新的堆相邻。

显然,合并时的顺序不同会导致合并的总代价不同。

找出一种合理的方法,使得将所有石子合并成一堆的代价最小,输出最小代价。

输入格式:

第一行一个整数\(N\)表示石子的堆数。

接下来的一行\(N\)个整数,第\(i\)个整数表示第&i&堆石子的质量。

输出格式:

一个整数,表示代价。

数据范围:\(1\leq N\leq 100\)

样例:
#in:
4
1 3 5 2
#out
22

解析

满足有限集最优化请读者自证

仍然从两个角度考虑:

状态表示

对于\(F(i,j)\)

集合:所有将区间\([i,j]\)合并成一堆的方案集合

属性:题目求的是最小值,所以\(min\)

状态计算

老套路,来看这个\(F(i,j)\)表示的集合:

仍然是考虑如何划分这个集合

考虑最后一个不同点

最后一次, 也就是合并到\([i,j]\)时,一定是由两个区间\([i,k]\)\([k,j]\)合并而来的。显然,\(k\in [i,j]\)

所以我们考虑以这个分界点 \(k\) 为划分依据,分成 \(j-i\) 类。

再来看一下合并的区间:

两个区间互不干扰,所以两边取\(min\),两边恰好是 \(F(i,k)\)\(F(k+1,j)\)

但是这只是两个子区间的最小代价,求 \(F(i,j)\) 还要加上这部分石子的总质量

于是\(F(i,j) = min(\ F(i,k+1) + F(k+1,j)\ ) + S_j-S_{i-1}\)\(S\)是石子重量的前缀和。

放代码:

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

const int N=310;

int n;
int s[N];
int dp[N][N];

int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>s[i];
		s[i]+=s[i-1];
	}
	if(n==1)//n=1的情况特判
	{
		cout<<0;
		return 0;
	}
	for(int l=2;l<=n;l++)//枚举区间长度
	{
		for(int i=1;i+l-1<=n;i++)
		{
			int j=i+l-1;
			dp[i][j]=0x3f3f3f3f;
			for(int k=i;k<j;k++)
			{
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
			}
		}
	}
	cout<<dp[1][n];
	return 0;
 }

练习

留一道题练练手

如果找不到感觉,可以再看看上面的过程,yxc老师的视频里也有讲这道题。

最长公共子序列

给定两个长度分别为 \(N\)\(M\) 的字符串 \(A\)\(B\),求\(A,B\)最长公共子序列长度。

输入格式

第一行两个整数\(N,M\)

第二行为一个长度为\(N\)的字符串,表示字符串 \(A\)

第三行为一个长度为\(M\)的字符串,表示字符串 \(B\)

字符串均由小写字母构成

输出格式

一个整数,表示最大长度。

样例
#in
4 5
acbd
abedc

#out
3


总结

DP是一个经验性的问题,闫氏DP分析法只是给出了一个分析角度,帮助人更好的分析考虑DP的有关问题,里面的状态表示设计仍然需要做题经验的积累才能顺利完成。道阻且长,但是经验多了自然水到渠成。

提高:动态规划中的各种模型<一>

posted @ 2020-09-09 17:54  RemilaScarlet  阅读(3490)  评论(6编辑  收藏  举报