2023-2练习

动态规划

阿巴阿巴

核心套路

dp

问题:集合如何划分

  • 一般原则:

      不重(不一定都要满足,一般求个数时要满足)
      
      不漏(必满足)
    
  • 如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来。

  • 一般地,正序寻找最后一个不同点,倒序寻找第一个不同点

2023-2-13

特点:每个物品仅能使用一次

思:每个物品有选和不选两种状态,若暴力枚举,时间复杂度为\(O(2^n)\),需对其进行优化。

  1. 设置状态\(f[i][j]\)

表示从编号为1~i的物品中选择且物品总体积\(v≤j\),它的值是这个集合中每一个选法的最大值

  1. 状态转移方程

不选:值为前一个集合最大值 \(f[i][j]=f[i-1][j]\)

选择:\(f[i][j]=f[i-1][j-v[i]]+w[i]\)

所以转移方程为\(f[i][j]=max(f[i-1][j], f[i-1][j-v[i]]+w[i])\)

不要忘记设置初始状态\(f[0][0]=0\)

//代码如下
f[i][j]=f[i-1][j];
if(j>=v[i]){//注意j-v[i]不能越界,背包满了
	f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}

小寄巧: memset(f,0xcf,sizeof f);初始化为负无穷

二维朴素版本
#include <bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,V,w[N],v[N],f[N][N];
int main()
{
	cin>>n>>V;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	memset(f,0xcf,sizeof f);
	f[0][0]=0; 
	for(int i=1;i<=n;i++){
		for(int j=0;j<=V;j++){//正序倒序均可
			f[i][j]=f[i-1][j];
			if(j>=v[i]){
				f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
			}
		}
	}
	int ans=0;
	for(int i=0;i<=V;i++) ans=max(ans,f[n][i]);
	cout<<ans;
	return 0;
}

将状态\(f[i][j]\)优化到一维\(f[j]\),实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态\(f[i][j]\)可以求得任意合法的\(i\)\(j\)最优解,但题目只需要求得最终状态\(f[n][m]\),因此我们只需要一维的空间来更新状态。

1 .状态\(f[j]\)定义:\(N\)件物品,背包容量j下的最优解。

2 . 注意枚举背包容量\(j\)必须从\(V\)开始。

3 . 为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态\(f[i][j]\)是由上一轮i - 1的状态得来的,\(f[i][j]\)\(f[i - 1][j]\)是独立的。而优化到一维后,如果我们还是正序,则有\(f[较小体积]\)更新到\(f[较大体积]\),则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

4.例如,一维状态第\(i\)轮对体积为 \(3\)
的物品进行决策,则\(f[7]\)\(f[4]\)更新而来,这里的\(f[4]\)正确应该是\(f[i - 1][4]\),但从小到大枚举j这里的f[4]在第i轮计算却变成了\(f[i][4]\)。当逆序枚举背包容量j时,我们求\(f[7]\)同样由\(f[4]\)更新,但由于是逆序,这里的\(f[4]\)还没有在第\(i\)轮计算,所以此时实际计算的\(f[4]\)仍然是\(f[i - 1][4]\)

5.简单来说,一维情况正序更新状态\(f[j]\)需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

\(f[i-1][j]=f[j],f[i-1][j-v[i]]=f[j-v[i]]\)

状态转移方程为:\(f[j] = max(f[j], f[j - v[i]] + w[i])\)

一维版本
#include <bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,V,v[N],w[N],f[N];
int main()
{
	cin>>n>>V;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++){
		for(int j=V;j>=v[i];j--){//注意逆序
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
   	//cout<<f[V];
	int ans=0;
	for(int i=0;i<=V;i++) ans=max(ans,f[i]);
	cout<<ans;
	return 0;
}


特点:每个物品可重复使用

不选:值为前一个集合最大值 \(f[i][j]=f[i-1][j]\)

选择:\(f[i][j]=f[i][j-v[i]]+w[i]\)

所以转移方程为\(f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])\)

  • 想求 \(f[i][j]\),
    要先求\(f[i][j-v[i]]\),,两者都是\(f[i]\),也就是在同一层,所以只能正序更新。

二维朴素版

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int n, V;
int dp[N][N], v[N], w[N];
int main(){
    cin >> n >> V;
    for(int i = 1; i <= n; i ++ )
        cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i ++ ){
        for(int j = 0; j <= V; j ++ ){//注意正序
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i])
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][V] << endl;
    return 0;
}

与01背包类似

\(f[i][j]=f[j],f[i][j-v[i]]=f[j-v[i]]\)

转移方程为$ f[j]=max(f[j],f[j-v[i]]+w[i])$

与01背包不同,要正序

一维版

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int n, V;
int dp[N];
int main(){
    cin >> n >> V;
    for(int i = 1; i <= n; i ++ ){
        int v, w;
        cin >> v >> w;
        for(int j = v; j <= V; j ++ ){//注意正序
                dp[j] = max(dp[j], dp[j - v] + w);
        }
    }
    cout << dp[V] << endl;
}
逐步优化

2023.2.14

区:子段和子序列

\(a={1,1,4,5,1,4}\)//doge

子段:连续的一段,\(e.g.\) [1,1,4],[1,4,5] ,[5,1,4]

子序列:不一定连续,\(e.g.\) [1,4,4],[1,1,1],[1,5,1]

子段一定是子序列

这里上升子序列是严格单调递增(不能相等

思:

从5向前找,有 [5] ,\([4,5],[1,4,5],[1,4,5]\)符合上升条件

注意:只有自己也是一种情况

设置状态:\(f[i]\)表示从第\(i\)个数向前找,它的值为最大上升子序列的长度

状态转移:

符合升序,则选择 \(f[i]=f[j]+1(i>j)\)

所以方程为\(f[i]=max(f[i],f[j]+1)\)

  • 模板
#include <bits/stdc++.h>
using namespace std;
int f[10000],a[10000],n;
int main(){
	memset(a,0xcf,sizeof a);//防止数据的值为负数或0,出现问题
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++){
		for(int j=i-1;j>=0;j--){//从i-1往前扫,j=0即可计算只有i的情况f[j]+1=1
			if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);//要符合升序
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,f[i]);//f[n]未必是最大值
	cout<<ans;
	return 0;
}

2023.2.16

  • 区间DP

P1880 [NOI1995] 石子合并

区间DP+前缀和

规定每次只能选相邻的2堆合并成新的一堆,是连续的。

所以数组开二倍,变成一个环。

1

样例:4 5 9 4

可以1和2合并,3和4合并,2和3合并,总价=(4+5)+(9+4)+(4+5+9+4)=44,

也可以2和3合并,1和2,3和4,总价=(5+9)+(4+5+9)+(9+5+4+4)=54,

求出总价最大/最小。
1

设状态:
\(f[i][j]\) 表示一个区间的左端点i,为右端点j,存最大方案数,(\(g[i][j]\)反之.

转移:

枚举区间(l,r),然后枚举l和r之间的k点。

\(f[l][r]=\max(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1])\)

区间DP应用情况:

6

#include <bits/stdc++.h>

#define cin std::cin
#define cout std::cout
#define endl std::endl
#define max std::max
#define min std::min

namespace lcj{
const int N=210;
int n,a[N],f[N][N],g[N][N],s[N];
void main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i+n]=a[i];
	}
	for(int i=1;i<=2*n;i++) s[i]+=s[i-1]+a[i];
	memset(f,0xcf,sizeof f);memset(g,0x3f,sizeof g);//初始化
	for(int i=1;i<=2*n;i++) f[i][i]=g[i][i]=0;
	for(int i=2;i<=n;i++){//枚举长度为i的区间
		for(int l=1;l+i-1<=2*n;l++){//注意细节
			int r=l+i-1;//计算r,不要忘-1。
			for(int k=l;k<r;k++){//k<r
				f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
				g[l][r]=min(g[l][r],g[l][k]+g[k+1][r]+s[r]-s[l-1]);
			}
		}
	}
	int maxv=INT_MIN,minv=INT_MAX;//INT_MIN是个常数,int的最小值,选最大值用,不能加,会爆,
	for(int i=1;i<=n;i++){
		maxv=max(maxv,f[i][i+n-1]);//区间长度i+n-1,不要忘-1,EXP++;
		minv=min(minv,g[i][i+n-1]);
	}
	cout<<minv<<endl<<maxv;
}
}
signed main(){
	lcj::main();
	return 0;
} 


参考:

AcWing 2. 01背包问题(看这篇就完事)

AcWing 2. 01背包问题(状态转移方程讲解)

AcWing 2. 01背包-打印dp理解用一维数组为何要逆序更新

AcWing 2. 闫氏DP分析法- y总讲解笔记

bilibili:最长上升子序列

2.4动态规划—石子合并问题

闫氏DP分析

posted @ 2023-04-12 23:11  CodeFirefly  阅读(38)  评论(0)    收藏  举报