ACM寒假第六讲:dp

ACM寒假第六讲

最大子段和

洛谷 - P1115

思路

最好想到的思路是求出所有子段和,然后比较得出和最大的。但是这样的时间复杂的非常的高

以a1为起点的子段和分别为

a1

a1+a2

a1+a2+a3

a1+a2+a3+a4

...

a1+a2+a3+a4+...+an

以a2为起点的子段和分别为

a2

a2+a3

a2+a3+a4

...

a2+a3+a4+...+an

我们发现a1为起点的所有子段和和a2为起点的所有子段和每个式子只差一个a1。那是不是所有子段和的最大值也只差一个a1?

并非如此,a1-a1=0,,如果a1为起点的所有子段和的最大值正好是a1,那么此时得到的a2为左端点的所有子段和的最大值就不一定是0,可能在其中产生了,但是反过来想以a1为左端点的所有的子段和的最大值就等于 max(0,MAX_a2)+a1;这样我们倒着向前推,就可以逐渐得到分别以a1,a2,a3,a4...an为起点的子段和的最大值,那么再在这群最大值中选出来一个最大的就是所有子段和的最大值。

代码

#include<iostream>
#include<climits>
using namespace std;
const int MAX=200001;
int n,arr[MAX],dp[MAX],ans=INT_MIN,max_sum=0;
int main(){
	cin>>n;
	for(int i=0;i<n;++i) cin>>arr[i];
	dp[n-1]=arr[n-1];
	for(int i=n-1;i>0;--i)
		dp[i-1]=max(0,dp[i])+arr[i-1];
	for(int i=0;i<n;++i) 
		if(dp[i]>ans) ans=dp[i];
	cout<<ans;
	return 0;
}

采药

洛谷 - P1048

思路

本题可以想象成一个背包问题。背包的容量就是最大时间。我们背包里面能装下剩余草药的时间小于剩余时间的任何一种草药。

我用dp[i] [j] 表示编号为i的草药装进已经累计时间为 j 的一种方案时的获得的价值

那么dp[i] [j+ti] = max(dp[i-1] [j]+vi,dp[i-1] [j+ti]);但是要注意,这里的 dp[i-1] [j] 这里必须是已经存过值的,要不然这个 j 是没有意义的,如果这里面没有存值说明 j 的时间里没有采任何药材,但 j 时间里不可能什么药材都没采,但是特殊情况时 j == 0的情况,这种是有意义的.

这样逐步递推,直到所有药材都被判断过一次后取最后一行dp[n] [i] 的最大值就是结果,当然要注意在背包容量内。

代码

#include<iostream>
using namespace std;
const int MAX=1001,MAXM=101;
int T,M,t,s,dp[MAXM][MAX],ans=0;
int main(){
	cin>>T>>M;
	for(int i=1;i<=M;++i){
		cin>>t>>s;
		for(int j=1;j<=T;++j) dp[i][j]=dp[i-1][j];
		if(t>T) continue;
		for(int j=0;j+t<=T;++j){
			if(j==0||dp[i][j]){//已经走过的时间为j的基础上,如果dp[i][j]==0说明这个时间没出现过,那么没有累计意义 
				dp[i][j+t]=max(dp[i-1][j+t],dp[i-1][j]+s);	 	
			}
		}
	}
	for(int i=1;i<=T;++i){
		ans=ans>dp[M][i]?ans:dp[M][i];
	}
	cout<<ans;
	return 0;
}

宝物筛选

洛谷 - P1776

思路

本题和上题的大体思路类似,而唯一不同的是每一类型的宝物的数量不止一个,如果拆开来看的话,总共的宝物的类型就会显得太多,这样O(N*M)的时间复杂度就会超时。

我对此的改进是

		for(int j=0;j+w<=W;++j){
			if(j==0||dp[i-1][j]){
				for(int k=1;k<=m&&j+k*w<=W;++k){
					dp[i][j+k*w]=max(dp[i][j+k*w],dp[i-1][j]+k*v);	
				}
			}
		}

纠结点在于每种宝物多的数量。这样多的也当成一种新的类型多余的计算在哪里呢?

本来是 dp[i] [j+w] = dp[i] [j] + v

此时我们再加入一件相同的宝物,我们还要再算一次 dp[i] [j+w] = dp[i] [j] + v;然后再算dp[i] [j+2w] = dp[i] [j+w] + 2v;

dp[i] [j+kw] = dp[i] [j] + kw;当 j 不同时,我们可以确定不同的 数列,所以 j 在递增时对结果是不会产生交叉影响的。

所以我们在第一次识别到 dp[i] [j] 上面有值的时候就顺便把 w 的其他倍数的合法位置也进行更新。,上面论述递推公式还需要和本地值再取一次最大值。这样本题可以将时间复杂度控制在O(W^2)级别内 为10的8次方到9次方,小于题目的时间限制。

代码

#include<iostream>
using namespace std;
const int MAXN=101,MAXW=40001;
int n,W,v,w,m,dp[MAXN][MAXW],ans;
int main(){
	cin>>n>>W;
	for(int i=1;i<=n;++i){
		cin>>v>>w>>m;
		for(int j=0;j<=W;++j) dp[i][j]=dp[i-1][j];
		for(int j=0;j+w<=W;++j){
			if(j==0||dp[i-1][j]){
				for(int k=1;k<=m&&j+k*w<=W;++k){
					dp[i][j+k*w]=max(dp[i][j+k*w],dp[i-1][j]+k*v);	
				}
			}
		}
	}
	for(int i=1;i<=W;++i){
		ans=max(ans,dp[n][i]);
	}
	cout<<ans;
	return 0;
}

最长公共子序列

洛谷 - P1439

思路

本题看似是求最长公共子列,实则是再求最长非降子列的大小。

求最长公共子列的标准代码

#define MAX 100000000//总之在可以申请的范围内,长度大于字符串的最长长度
strin text1,text2;
cin>>text1>>text2;
int dp[MAX][MAX]={0};//dp[i][j]表示第一个字符串的前i个元素和第二个字符串的前j个元素最长的公共子序列的长度
for(int i=1;i<=text1.size();++i){
    for(int j=1;j<=text2.size();++j){
        if(text1[i]==text2[j])
            dp[i][j]=dp[i-1][j-1]+1;
        else dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
    }
}

可以发现时间时间复杂度是O(N*M),空间复杂度也是这个,暂且不论时间复杂度,就空间复杂度而言我们也开不出来这么大的数组,一定会空间爆掉,所以这道 题不只是运用求最长公共子序列的算法,还要借助数字排列的特点。

怎么考虑呢?我们从第一个排序列中从左到右选一个子序列,什么叫公共子序列呢?就是相同数字之间的相对位置是一样的。所以我们需要保证从第二个序列里面从左到右找到这几个数字后他们的顺序和第一个是一样的,但这样还是做不了题,我们还可以将第二个子序列按第一个子序列的方式拍好后,看第二个子序列他们的所在位置(数组的下标)是否是递增的,这样我们就可以把第二个数字的位置对应到第一个数列相同数字的位置,然后在这个下标数列中寻找最长的非降子列的长度。

int dp_LIS[MAX],min_last[MAX];
/*
dp_LIS[MAX]存的是下标到以前的一段数列的最长非降数列的长度,min_last[MAX]存的的是最长长度为下标的非降子列中末尾值最小的末尾值,这样就能然后面的数字进尽可能加到目前的最长非降子列的后面,较短的末尾值变小了,这样当前的最长非降子列的末尾元素也能更新为更小的值这样最长非降子列更容易增加。
*/
for (int i=0;i<n;++i) {
        //k_v[text2[i]]为每一项
        int l=0,r=dp_LIS[i];//dp存的是个数
        while (l<r) {//二分求小于x的值里的最大值
            int mid=(l+r+1)>>1;
            if (min_last[mid]<k_v[text2[i]]) l=mid;
            else r=mid-1;
        }
        min_last[++r]=k_v[text2[i]];
        dp_LIS[i+1]=std::max(dp_LIS[i],r);
    }

代码

#include <algorithm>
#include<stdio.h>
#define MAX 100001
/*
 * 本题因为数据范围是到10^5,所以可以直接用数组来存位置,直接用数组访问
 */
int n,text1[MAX],text2[MAX],k_v[MAX],dp_LIS[MAX],min_last[MAX];
int main() {
    scanf("%d",&n);
    for (int i=0;i<n;++i) {
        scanf("%d",&text1[i]);
        k_v[text1[i]]=i;//值为text1[i]被存放的位置为i
    }
    for (int i=0;i<n;++i) scanf("%d",&text2[i]);
    //动态规划得到LIS
    for (int i=0;i<n;++i) {
        //k_v[text2[i]]为每一项
        int l=0,r=dp_LIS[i];//dp存的是个数
        while (l<r) {//二分求小于x的值里的最大值
            int mid=(l+r+1)>>1;
            if (min_last[mid]<k_v[text2[i]]) l=mid;
            else r=mid-1;
        }
        min_last[++r]=k_v[text2[i]];
        dp_LIS[i+1]=std::max(dp_LIS[i],r);
    }
    printf("%d",dp_LIS[n]);
    return 0;
}

Kevin and Puzzle

CodeForces - 2061C

思路

5
5 1 5 2 5

这个数列的前 i 个配置的个数,我们只看前面配置的最后一个如果是说谎者和不是说谎这的情况

如果前面配置的最后一个是真言者,那么新加的这个如果是真言者,那么a[i]==a[i-1]才成立,那么前面所有真言者的数量加到dp[i]的真言者的数量里

如果不成立那么前面真言的情况后面对应这个为说谎者,即前面的所有真言者的数量加到dp[i]的说谎者的数量里面

如果前面配置的最后一个是说谎者,那么当前这个和说谎者的前一个一定是真言者,当前的这个真言者一定比前面一个真言者的数量多一

a[i]==a[i-2]+2 ,,如果不成立,那么说明前面一个说谎者的数量丢失了,不能往下走下去了。

然后将这轮的说谎者和真言者的数量加起来就是到当前长度时,配置的数量。

注意dp[1].如果第一个输入是 0 ,那么第一个可能是真言者,也可能是说谎者,dp[1]=2;但是如果第一个输入大于 0 那么第一个一定是说谎者dp[1]=1;

代码

#include<iostream>
#include<vector>
using namespace std;
#define ll long long
const ll mod=998244353;
void solve() {
    int n;
    ll _true=1,_false=1;
    cin>>n;
    vector<ll> v(n+1),dp(n+1,0);
    for (int i=1;i<=n;++i) cin>>v[i];
    if (!v[1])
        dp[1]=2,_true=1,_false=1;
    else
        dp[1]=1,_true=0,_false=1;
    for (int i=2;i<=n;++i) {
        ll diff_true=0,diff_false=0;
        if (_true) {//加这个if思路更清晰些
            diff_false+=_true;
            if (v[i]==v[i-1]) diff_true+=_true;
        }
        if (_false&&v[i]-1==v[i-2]) {//说明0的后面可以是1
            diff_true=(_false+diff_true)%mod;
        }
        _true=diff_true,_false=diff_false;
        dp[i]=(diff_true+diff_false)%mod;
    }
    cout<<dp[n]<<endl;
}
int main() {
    int T=1;
    cin>>T;
    while (T--) solve();
    return 0;
}

World is Mine

CodeForces - 1987D

思路

alice的策略就是吃当前能吃的蛋糕的美味值最小的蛋糕。bob的策略是在alice吃到某个美味值得蛋糕之前将这个美味值得蛋糕吃完。但是具体吃哪一个符合条件得蛋糕才是最佳得需要最终判断,前面只要保证每种情况bob吃得是合法得就行。

对于每块蛋糕bob可以选择吃或者不吃,而对于alice而言,有的吃就吃,没得吃跳过这个级别得来吃。

每一个美味值得蛋糕可能会有重复得,但是对于alice而言重复得蛋糕和一个这样得蛋糕没什么区别,而对于bob而言他吃也是一下吃完这个美味值得所有蛋糕。所以我们可以统计将这些蛋糕放进桶里,相同美味值得蛋糕放进一个桶里。

对于bob来说每一个蛋糕要么吃(有约束)要么不吃,有点像背包问题,对于每一个物品要么装进去,要么不装进去。

这里我们可以将bob最多能吃得蛋糕的数量(n/2)最为背包的容量,不同美味值的蛋糕作为不同的物品。这题虽然同样美味值的蛋糕可能不止一个,但是不同于“宝物筛选”那题的是本题既然决定要吃某个蛋糕,那么就一定会把这种类型的蛋糕吃完。所以可以将某种美味值的几个蛋糕堪称一个整体。设计一个dp[i] [j] ;其中 i 表示蛋糕的美味值编号,j表示当选择美味值为 i的蛋糕时,bob已经吃了的蛋糕的数量,这里的 j 同样是累计含义,所以算法要能够判断这个 j 是否是有意义的,或者算法当 j 无意义时,不会影响结果。总体的dp[i] [j]表示的是当前状态下alice吃的蛋糕的数量。

所以当编号为 i 的蛋糕的数量为 cnt[i] 时,如果cnt[i] 的值为零,那么把状态复制下来就好,保证状态可以和后面的续上,如果cnt[i]的值不为 0 ,那么就要看 bob能不能吃这个蛋糕,并且bob如果能吃这个蛋糕的话对状态产生了哪些影响。

因为bob是后手的,所以当bob要吃蛋糕时,吃完这个美味值的蛋糕的数量应该小于等于alice已经吃过的蛋糕的数量。并且不会超过“背包容量”。但是如果bob不吃这个美味值的蛋糕时alice得吃蛋糕的数量应该加一,但是此时的bob的吃蛋糕的数量还是 j 不变。

这里说明一点。题干里面时每轮bob都会吃一个蛋糕,而我们这里假设bob可以积攒吃蛋糕的数量,当bob想要吃这个蛋糕时一下把自己积攒的吃蛋糕的能力都释放出来,所以我们可以堪称alice和bob是同时来判断某一个编号的蛋糕的。

状态方程

vector<vector<int> > dp(n+1,vector<int>(n+1,1e15));
/*
先初始化dp为一个很大的值,但注意不要是极限值,因为后面会拿这个值去四则运算,一旦加个1,数值就会溢出,就不准了,所以需要找一个很大,但是不会影响计算结果的大数。
*/
if(!cnt[i]){
    dp[i][j]=min(dp[i-1][j],dp[i][j]);
}
//保证新加入的值不会超过背包的容量,并且bob吃完后,bob吃的蛋糕的数量不会超过alice吃过的蛋糕的数量
if(j+cnt[i]<=n/2&&j+cnt[n]<=dp[i-1][j])
    dp[i][j+cnt[i]]=min(dp[i][j+cnt[i]],dp[i-1][j]);
/*bob吃了,那么alice在这个蛋糕上吃过蛋糕的数量就不会有增长。注意开始将dp赋值为了一个很大的数字,所以这里取最小值,同时也是为了 无效的 j 对结果不会产生影响。无效的 j 对应的dp存的值还是那个大数*/
	//这里考虑bob没有吃完这个蛋糕的情况
	dp[i][j]=min(dp[i-1][j]+1,dp[i][j]);//如果bob没吃,那么alice吃过的蛋糕的数量+1

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
void solve() {
    int n;
    cin>>n;
    vector<int> cnt(n+1,0);
    //dp[i][j]整体表示当判断编号为i的蛋糕时,如果bob吃了j块蛋糕,alice吃掉的蛋糕的数量
    vector<vector<int>> dp(n+1,vector<int>(n+1,1e15));//这里的初值不能复制为INT_MAX,因为一旦加一个数字就会变成溢出
    dp[0][0]=0;
    for (int i=0;i<n;++i) {
        int x;cin>>x;
        ++cnt[x];
    }
    for (int i=1;i<=n;++i) {//蛋糕编号
        for (int j=0;j<=n/2;++j) {
            if (!cnt[i]) {//如果当前编号的蛋糕没有,就将状态复制下来
                dp[i][j]=min(dp[i-1][j],dp[i][j]);
                continue;
            }
            if (j+cnt[i]<=n/2&&dp[i-1][j]>=j+cnt[i])//如果bob能吃这块蛋糕并且吃掉了的话
                dp[i][j+cnt[i]]=min(dp[i][j+cnt[i]],dp[i-1][j]);
            //bob不吃这块蛋糕
            dp[i][j]=min(dp[i][j],dp[i-1][j]+1);
        }
    }
    int ans=1e15;
    for (int i=0;i<=n/2;++i) {
        ans=min(ans,dp[n][i]);
    }
    cout<<ans<<endl;
}
signed main() {
    ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
    int T=1;
    cin>>T;
    while (T--) solve();
    return 0;
}

学习总结

动态规划的核心思想是用空间换时间,记录下来一些公用的数据来减少时间的损耗。将一个复杂的问题分割成数个相同的子问题。逐步求优,最后保证优中选优

最长公共子列

#define MAX 100000000//总之在可以申请的范围内,长度大于字符串的最长长度
strin text1,text2;
cin>>text1>>text2;
int dp[MAX][MAX]={0};//dp[i][j]表示第一个字符串的前i个元素和第二个字符串的前j个元素最长的公共子序列的长度
for(int i=1;i<=text1.size();++i){
    for(int j=1;j<=text2.size();++j){
        if(text1[i]==text2[j])
            dp[i][j]=dp[i-1][j-1]+1;
        else dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
    }
}

最长非递减子列

int dp_LIS[MAX],min_last[MAX];
/*
dp_LIS[MAX]存的是下标到以前的一段数列的最长非降数列的长度,min_last[MAX]存的的是最长长度为下标的非降子列中末尾值最小的末尾值,这样就能然后面的数字进尽可能加到目前的最长非降子列的后面,较短的末尾值变小了,这样当前的最长非降子列的末尾元素也能更新为更小的值这样最长非降子列更容易增加。
*/
for (int i=0;i<n;++i) {
        //k_v[text2[i]]为每一项
        int l=0,r=dp_LIS[i];//dp存的是个数
        while (l<r) {//二分求小于x的值里的最大值
            int mid=(l+r+1)>>1;
            if (min_last[mid]<k_v[text2[i]]) l=mid;
            else r=mid-1;
        }
        min_last[++r]=k_v[text2[i]];
        dp_LIS[i+1]=std::max(dp_LIS[i],r);
    }

背包dp

常规是设置dp[i] [j];这里的 i 表示物品的编号,而 j 表示背包的容量;dp[i] [j] 表示这一状态的答案值,可能是价值,也可能是数量

找两个状态之间的关系,然后用状态转移方程表示出来。但理论是简单的,做题时能够辨识出来并且运用是困难的,就像World is Mine这道题一样,“背包”非常的隐晦。

posted @ 2025-02-16 22:51  Buy-iPhone  阅读(5)  评论(0)    收藏  举报