动态规划 之《从入门到入土》

bushi

动态规划的几个模板 and 例题

背包问题

01背包

顾名思义,一个东西只有选和不选两种选择。
求体积一定的包里能放的最大质量。

	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=w[i];j--)//w[i]表示物品 i 的体积
		{
			f[j]=max(f[j],f[j-w[i]]+v[i]);//v[i]表示物品 i 的质量
            //f[j]表示 背包容量为 j 时最多能放的质量
		}
	}

为什么要逆序

首先,通过上一个问题,我们确认了我们目前一维的dp数组,保存的是确认过的最新一层的数据,即上一层的数据。
当我们计算当前层时,对于二维时的状态转移方程有
\(dp_{i,j} = max\) \(\{dp{i,j}, dp_{i-1,j-v_i} + w_i\}\);
可以看到,\(dp_{i-1,j-v_i}+ w_i\) 使用的上一层的原始数据\(dp_{i-1}\),而我们使用一维的状态转移方程时有
\(dp_{j} = max\) \(\{dp{j}, dp_{j-v_i} + w_i\}\);
当我们从小到大更新是, 因为 \(j - v_i\) 是严格小于 \(j\) 的,所以我们可以举个例子:
\(dp_3 = max\) \(\{dp_3, dp_2 + 1\}\), 因为我们是从小到大更新的,所以当更新到 \(dp_3\) 的时候,\(dp_2\)已经更新过了,已经不是上一层的 \(dp_2\)
而当我们逆序更新时有,举例 \(dp_8 = max\) \(\{dp_8, dp_6 + 2\}\)。当更新 \(dp_8\) 时,\(dp_6\) 还没有被更新,还是上一层的数据,这样才能保证没有读入脏数据。

以上来自 AcWing

完全背包

与 01 背包的区别就是:同一个物品可以被放多次

for(int i=1;i<=n;i++)
	{
		for(int j=w[i];j<=m;j++)//这里的枚举顺序要改变一下
		{
			f[j]=max(f[j],f[j-w[i]]+v[i]);
            //f[j]表示总体积是 j 时最大价值是多少
		}
	}

枚举顺序改变的原因:

每次再使用 \(f_j\) 的时候状态就是已经更新过的

多重背包

在完全背包的基础上,每个物品是有限定数量的。
(1) 可以把每种物品看成 cnt 个物品,只不过他们的质量体积都相同,这样就能转化为 01 背包问题。
(2) 每个物品有 不选选一个选两个、………… 选 cnt 个,加一重循环就能实现。
好像两个运行时间差不多?
代码(方法 2 )

 for(int i=1;i<=n;i++)      
    for(int k=1;k<=cnt[i];k++) //物品数量,可以看作一个物品有好多份,01背包 
	{
		for(int j=m;j>=k*w[i];j--)//容量
		{
			f[j]=max(f[j],f[j-w[i]]+v[i]);
		}
	}

分组背包

思路:每一组物品有 \(cnt_i + 1\) 种决策:
不选、选第一个、选第二个、…… 选第 cnt 个。(01背包)

  • 代码
for(int i=1;i<=n;i++)//枚举每一组
{
        int cnt;
        cin>>cnt;
        for(int j=1;j<=cnt;j++)
        {
            cin>>w[j]>>v[j];//输入
        }
        for(int j=m;j>=0;j--)//枚举背包容积
        {
            for(int k=1;k<=cnt;k++)//枚举每一种决策
            {
                if(j>=w[k]) f[j]=max(f[j],f[j-w[k]]+v[k]);
            }
        }
}
    cout<<f[m]<<endl;

例题:

这里 https://www.luogu.com.cn/training/572428

子序列问题:

最长上升子序列长度

  • 朴素做法
    \(dp_i\) 表示 以 \(i\) 点为结尾的最长上升子序列的长度。
for(int i=1;i<=n;i++)
	{
		dp[i]=1;
		for(int j=1;j<i;j++)
		{
			if(a[j]<a[i])dp[i]=max(dp[i],dp[j]+1);
		}
		maxn=max(maxn,dp[i]);
	}
  • 优化:
    如果 a_i 大于原序列末尾,就可以把他直接加上;
    如果小于原序列末尾,二分查找到此时序列中第一个小于等于它的值,替换掉。

理由:
如果y在末尾,由于y < a[i],所以y后面能接的不如a[i]多,y让位给a[i]可以让序列更长
如果y不在末尾,那y有生之年都不会再被用到了,直接踹了y就行,y咋样,who care?

p[0]=-1;
for(int i=1;i<=n;i++) //求 最长上升子序列的长度 
{
	if(a[i]>p[cnt])p[++cnt]=a[i];//直接加在后面
	else{         // 查找第一个 >= a[i] 的元素的位置 
		int left=1,right=cnt;
		while(left<right)
		{
			int mid=left+right>>1;
			if(p[mid]>=a[i]) right=mid;
			else left=mid+1; 
		}
		p[left]=a[i]; //替换掉
	}
} 

其他什么上升下降都差不多,就不再说了

最少的不上升子序列的个数

Dilworth 定理

对偏序集 $ \langle A, \le \rangle$,设 \(A\) 中最长链的长度是 \(n\),则将 \(A\) 中元素分成不相交的反链,反链个数至少是 \(n\)

人话翻译:最少的不上升子序列的个数就是最长上升子序列的长度
具体证明我不会请参考 https://www.luogu.com.cn/article/znt20wu4

for(int i=1;i<=n;i++)//求最长不下降子序列 
	{
		if(a[i]<=d[cnt]) d[++cnt]=a[i];
		else{
			int left=1,right=cnt;
			while(left<right)//查找第一个 < a[i] 的元素的位置 (因为等于的时候不需要替换)
			{
				int mid=left+right>>1;
				if(d[mid]<a[i])right=mid;
				else left=mid+1;
			}
			d[left]=a[i];
		}
	}

区间DP

性质

  • 合并:将两个或者多个部分进行整合,当然也可以反过来;
  • 特征:能将问题分解成两两合并的形式;
  • 求解:对整个问题设最优解,枚举合并点,将问题分解成左右两个部分,最后合并两个部分的最优值得到原问题的最优值。

定义:

定义 \(f_{i,j}\) 表示将下标位置 i 到 j 的所有元素合并能获得的价值的最大值,那么 \(f_{i,j} = max\) \(\{f_{i,k}+f_{k+1,j}+cost\}\),cost 为将这两组元素合并的价值。

以上来自 OIWIKI

例题:

NOI1995 石子合并

  • 观察到是环,首先 ctrl + c,ctrl + v 复制一份接在后面
  • 最普通的算法O(n^3):224ms
    其中,\(dp_{i,j}\) 代表 \(i\)\(j\) 堆的最优值,\(sum_i\) 代表第 \(1\) 堆到第 \(i\) 堆的数目总和。有:\(dp_{i,j}=min\) \(\{dp_{i,j},dp{i,k}+dp_{k+1,j}\}\) \(+sum_j-sum_{i-1}\)
  • 代码:17ms
#include<bits/stdc++.h>

using namespace std;

int n;
int a[250];
int sum[250];
int dp[250][250];
int dp2[250][250];
int minn=INT_MAX;
int maxn=INT_MIN;

signed main()
{
	memset(dp2,0x3f3f3f,sizeof(dp2));
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		a[n+i]=a[i];//复制一份接在后面 
	}
	int N=2*n;
	for(int i=1;i<=N;i++)
	{
		sum[i]=sum[i-1]+a[i];
		dp2[i][i]=0;
	}
	for(int k=2;k<=n;k++) //区间长度
	{
		for(int i=1;i<=N-k+1;i++) //枚举左端点 
		{
			int j=i+k-1;
			for(int p=i;p<j;p++) //枚举中间的序号
			{
				dp[i][j]=max(dp[i][j],dp[i][p]+dp[p+1][j]+sum[j]-sum[i-1]); //最大值 
				dp2[i][j]=min(dp2[i][j],dp2[i][p]+dp2[p+1][j]+sum[j]-sum[i-1]); //最小值 
			 } 
		}
	}
	for(int i=1;i<=n;i++)//开始点 
	{
		int p=i+n-1;//结束点
		maxn=max(maxn,dp[i][p]);
		minn=min(minn,dp2[i][p]);
	}
	cout<<minn<<endl;
	cout<<maxn<<endl;
	return 0;
 } 

练习题:

直接去洛谷搜标签

树形 DP

有依赖的背包问题

意思:给你一棵树,选取一个节点就必须选取他的父亲节点,求物品总体积不超过背包容量,且最大的总价值。

思路:

  • 定义 \(f_{i,j}\) 表示以 \(i\) 为根的子树,总共体积不超过 \(j\) 的最大价值

建议看: http://note.youdao.com/noteshare?id=07619acf27a64381650dbb9dc2000f68&sub=4FCC06D686104EA19BFFBF85E614343D

完结撒花!!!

csp_j 应该也就只能考这些了吧 qwq

posted @ 2024-08-30 15:08  lazy_ZJY  阅读(3)  评论(0编辑  收藏  举报