闫氏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( ext{子集的max} ))
当属性为 (count) 时,(F(i)=sum_ ( ext{子集的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)) 有关,且 (xleq 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&堆石子的质量。

输出格式:

一个整数,表示代价。

数据范围:(1leq Nleq 100)

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

解析

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

仍然从两个角度考虑:

状态表示

对于(F(i,j))

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

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

状态计算

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

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

考虑最后一个不同点

最后一次, 也就是合并到([i,j])时,一定是由两个区间([i,k])([k,j])合并而来的。显然,(kin [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的有关问题,里面的状态表示设计仍然需要做题经验的积累才能顺利完成。道阻且长,但是经验多了自然水到渠成。

原文地址:https://www.cnblogs.com/IzayoiMiku/p/13635809.html