动态规划一

复健Day4

动态规划(一)线性DP

1.数字三角形模型

此类题目的一般描述为给定一个n行的三角矩阵A,从第xy列出发,每次只能向下或者右下移动,到达底层后求某一属性的最优值

一般的解法都是设状态为dp[i][j],表示到达(i,j)时的属性最优值

(1)摘花生

https://www.acwing.com/problem/content/1017/

非常基础的模型,直接dp[i][j]转移即可,不过注意每次询问的时候都需先把dp数组清零

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 110
using namespace std;

int dp[maxn][maxn];
int f[maxn][maxn];
int dir[2][2]={{0,1},{1,0}};

int main()
{
	int t,m,n;
	cin>>t;
	while(t--)
	{
		memset(dp,0,sizeof(dp));
		cin>>n>>m;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				cin>>f[i][j];
			}
		}
		dp[1][1]=f[1][1];
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				for(int k=0;k<2;k++)
				{
					if(i+dir[k][0]>n||j+dir[k][1]>m) continue;
					dp[i+dir[k][0]][j+dir[k][1]]=
					max(dp[i+dir[k][0]][j+dir[k][1]],dp[i][j]+f[i+dir[k][0]][j+dir[k][1]]);
				}
			}
		}
		printf("%d\n",dp[n][m]);
	}
	return 0;
}

(2)最低通行费用

题目描述:

一个商人穿过一个N×N的正方形的网格,去参加一个非常重要的商务活动。

他要从网格的左上角进,右下角出。

每穿越中间1个小方格,都要花费1个单位时间。

商人必须在(2N1)个单位时间穿越出去。

而在经过中间的每个小方格时,都需要缴纳一定的费用。

这个商人期望在规定时间内用最少费用穿越出去。

请问至少需要多少费用?

注意:不能对角穿越各个小方格(即只能向上下左右四个方向移动且不能离开网格)。

输入格式:

第一行是一个整数,表示正方形的宽度N

后面N行,每行N个不大于100的正整数,为网格上每个小方格的费用。

输出格式:

输出一个整数,表示至少需要的费用。

数据范围:

1N100

注意到我们需要在2N1的时间内出去,实际也就是意味着我们只能往下或者往右跑,与上一题思路相同,设状态为dp[i][j]

然后代码也上一题相似

不过一开始做的时候我并没有想到这一性质,于是采用了一个三维数组维护最小费用dp[i][j][k]表示穿过(i,j)时间为k的最小费用,然后这个算法的时间复杂度应该是O(n 3)的,我认为是可以通过的,但是因为我没报算法提高班(T^T),无法验证对错与时间,不过这个代码也是通过了样例的,放在此处仅供大家参考

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 110
#define tox i+dir[t][0]
#define toy j+dir[t][1]
using namespace std;

int f[maxn][maxn];
int dp[maxn][maxn][maxn];
int dir[4][4]={{0,-1},{0,1},{-1,0},{1,0}};

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>f[i][j];
		}
	}
	memset(dp,0x3f,sizeof(dp));
	dp[1][1][1]=f[1][1];
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			for(int k=1;k<2*n-1;k++)
			{
				for(int t=0;t<4;t++)
				{
					if(tox<=0||tox>n||toy<=0||toy>n) continue;
					dp[tox][toy][k+1]=min(dp[tox][toy][k+1],dp[i][j][k]+f[tox][toy]);
					//printf("i:%d j:%d dp:%d\n",tox,toy,dp[tox][toy][k+1]);
				}
			}
		}
	}
	int Min=0x3f3f3f3f;
	for(int i=1;i<=2*n-1;i++) Min=min(Min,dp[n][n][i]);
	printf("%d\n",Min);
	return 0;
}
/*
5
1  4  6  8  10
2  5  7  15 17
6  8  9  18 20
10 11 12 19 21
20 23 25 29 33
*/

2.最长上升子序列模型

关于最长上升子序列,最暴力的求法是O(n 2)的复杂度

dp[i]表示以第i个数字结尾的最长上升子序列长度

#include<iostream>
#include<cstdio>
#define maxn 2005
using namespace std;

int a[maxn];
int dp[maxn];

int main()
{
	int n,ans=0;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++)
	{
		dp[i]=1;
		for(int j=1;j<i;j++)
		{
			if(a[i]>a[j]) dp[i]=max(dp[i],dp[j]+1);
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans<<endl;
	return 0;
}

在寻找序号小于i且比a[i]小的数字时,我们可以在此处进行优化,采用栈和二分的方法,将这一部分从O(n)优化为O(logn)的复杂度

我们维护一个栈,栈中的元素是可以更新之后的元素的dp值的最优的元素,每当我们枚举到一个a[i]时,如果a[i]大于栈顶元素,那么我们就把这个元素入栈,并用原来栈顶的元素去更新这个元素的sp值,而如果其小于等于栈顶元素,那么我们就采用二分的方法(注意二分的是栈里的元素)找到栈中最小的比它大的数,然后将其出栈,用这个元素去替换该元素。

我们在替换之后栈的长度并没有改变,但是在此处的数的潜力却变大了

栈的长度就是最长公共子序列

#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 10010
#define inf 0x3f3f3f3f
using namespace std;

int a[maxn];

int main()
{
    int n;
    cin>>n;
    int l,r,tot=0;
    a[tot]=-inf;
    while(n--)
    {
        int u;
        cin>>u;
        if(u>a[tot]) a[++tot]=u;
        else
        {
            l=1,r=tot;
            while(l<=r)
            {
                int mid=l+r>>1;
                if(u>a[mid]) l=mid+1;
                else r=mid-1;
            }
            a[l]=u;
        }
    }
    printf("%d\n",tot);
    return 0;
}

(1)合唱队形

https://www.luogu.com.cn/problem/P1091

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 10010
#define inf 0x3f3f3f3
using namespace std;

int w[maxn],a[maxn],up[maxn],down[maxn];

int main()
{
	int n;
	int tot=0;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	a[0]=-inf;
	for(int i=1;i<=n;i++)
	{
		if(w[i]>a[tot]) a[++tot]=w[i];
		else
		{
			int l=1,r=tot;
			while(l<=r)
			{
				int mid=(l+r)>>1;
				if(w[i]>a[mid]) l=mid+1;
				else r=mid-1;
			}
			a[l]=w[i];
		}
		up[i]=tot;
	}
	tot=0;
	memset(a,0,sizeof(a));
	a[tot]=-inf;
	for(int i=n;i>=1;i--)
	{
		if(w[i]>a[tot]) a[++tot]=w[i];
		else
		{
			int l=1,r=tot;
			while(l<=r)
			{
				int mid=(l+r)>>1;
				if(w[i]>a[mid]) l=mid+1;
				else r=mid-1;
			}
			a[l]=w[i];
		}
		down[i]=tot;
	}
	int ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,up[i]+down[i]-1);
	printf("%d\n",n-ans);
	return 0;
}

也就是正倒个进行一次最长不上升序列,最后得出满足条件的合唱队队形长度,最后的答案就是总人数减去队形的人数

这道题由于题目的数据范围限制,用O(n 2)用法会简便许多(此处我只是为了锻炼一下单调队列写法的熟练度故用此方法)

(2)木棍加工

https://www.luogu.com.cn/problem/P1233

这道题用到了dilworth定理(偏序集分解定理):对于任意有限偏序集,其最长链的元素数目必等于其最小反链划分中反链的数目

偏序关系:设P是一个集合,若P上的二元关系满足以下三个条件则称其为P上的偏序关系

1.自反性:aaaP

2.反对称性:a,bP,若abba,则a=b

3.传递性:a,b,cP,若abbc,则ac

偏序集:具有偏序关系的集合称为偏序集

:链是一种偏序集,其任意两个元素具有不具有可比性

反链:反链是一种偏序集,其任意两个元素不具有可比性

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#define maxn 5010
using namespace std;

int dp[maxn];

struct Node
{
	int h,w;
	bool operator < (const Node &rhs){
		if(h==rhs.h) return w>rhs.w;
		else return h>rhs.h;
	}
}node[maxn];

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>node[i].h>>node[i].w;
	int ans=0;
	sort(node+1,node+n+1);
	for(int i=1;i<=n;i++)
	{
		dp[i]=1;
		for(int j=1;j<i;j++)
		{
			if(node[i].w>node[j].w) dp[i]=max(dp[i],dp[j]+1);
		}
	}
	for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
	printf("%d\n",ans);
	return 0;
}

3.最长公共子序列

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 3010 
using namespace std;

int p[maxn][maxn];//p[i][j]记录转移到[i][j]时的前驱数组,1表示从左上方[i-1][j-1],2表示
//左方[i][j-1],3表示从上方转移[i-1][j]
int f[maxn][maxn];

int main()
{
	int n,m;
	string a,b;
	cin>>a>>b;
	n=a.length();m=b.length();
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(a[i]==b[j])
			{
				f[i][j]=f[i-1][j-1]+1;
				p[i][j]=1;
			}
			else
			{
				if(f[i][j-1]>f[i-1][j])
				{
					f[i][j]=f[i][j-1];
					p[i][j]=2;
				}
				else
				{
					f[i][j]=f[i-1][j];
					p[i][j]=3;
				}
			}
		}
	}
	int i=m,j=n,k=f[n][m];
	string s;
	while(i>0&&j>0)
	{
		if(p[i][j]==1)
		{
			s[k--]=a[i-1];
			i--;j--;
		}
		else if(p[i][j]==2) j--;
		else i--;
	}
	printf("%d\n",f[n][m]);
	for(int i=1;i<=n;i++) printf("%c",s[i]);
	return 0;
}

4.最长公共子串

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 2010
using namespace std;

int f[maxn][maxn];
string a,b;
int ans=0;

int main()
{
	cin>>a>>b;
	int n=a.length(),m=b.length();
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
			else f[i][j]=0;
			ans=max(ans,f[i][j]);
		}
	}
	printf("%d\n",ans);
	return 0;
}

5.编辑距离

这是普通的模板,时间复杂度为O(n 2)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 2010
using namespace std;

int f[maxn][maxn];
string a,b;
int ans=0;

int main()
{
	cin>>a>>b;
	int n=a.length(),m=b.length();
	for(int i=1;i<=n;i++)
	{
		f[i][0]=i;
		for(int j=1;j<=m;j++)
		{
			f[0][j]=j;
			if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1];
			else
			{
				f[i][j]=min(f[i-1][j-1],min(f[i][j-1],f[i-1][j]))+1;
			}
		}
	}
	printf("%d\n",f[n][m]);
	return 0;
}

我们呢可以采用滚动数组优化,使得其空间复杂度降为O(n)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 2010
using namespace std;

int f[maxn];
string a,b;
int ans=0;

int main()
{
	cin>>a>>b;
	int t1,t2;
	int n=a.length(),m=b.length();
	for(int j=1;j<=m;j++) f[j]=j;
	for(int i=1;i<=n;i++)
	{
		t1=f[0]++;//t1等价于f[i-1][0]
		for(int j=1;j<=m;j++)
		{
			t2=f[j];//t2存储更新前的f[j]
			if(a[i-1]==b[j-1]) f[j]=t1;
			else f[j]=min(t1,min(f[j-1],f[j]))+1;
			t1=t2;//t1等价于f[i-1][j-1] 
		}
	}
	printf("%d\n",f[m]);
	return 0;
}

例题:

1.大盗阿福

f[i][0/1]表示从第1家到第i家的店铺进行偷盗,上一家不抢/抢从而得到的最大现金数

则其转移是由f[i1][0],f[i1][1]从而得来的

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100010
using namespace std;

int f[maxn][2];
int w[maxn];

int main()
{
	int t;
	cin>>t;
	while(t--)
	{
		int n;
		cin>>n;
		memset(f,0,sizeof(f));
		for(int i=1;i<=n;i++) cin>>w[i];
		for(int i=1;i<=n;i++)
		{
			f[i][0]=max(f[i][0],max(f[i-1][1],f[i-1][0]));
			f[i][1]=max(f[i][1],f[i-1][0]+w[i]);
		}
		printf("%d\n",max(f[n][0],f[n][1]));
	}
	return 0;
}

2.股票买卖

https://www.acwing.com/problem/content/1057/

f[i][0/1]表示到第i天手上没有/有股票的到的最大利润

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100010
using namespace std;

int f[maxn][2];
int w[maxn];

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	f[1][0]=0,f[1][1]=-1*w[1];
	for(int i=2;i<=n;i++)
	{
		f[i][0]=max(f[i-1][0],f[i-1][1]+w[i]);
		f[i][1]=max(f[i-1][1],f[i-1][0]-w[i]);
	}
	printf("%d\n",max(f[n][0],f[n][1]));//实际上最大的一定是f[n][0]
	return 0;
}

3.股票买卖k笔交易

https://www.acwing.com/problem/content/1059/

f[i][0/1][j]表示到第i笔交易有无股票同时完成了j笔交易的最大利润值

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100010
using namespace std;

int f[maxn][2][110];
int w[maxn];

int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>w[i];
	memset(f,-0x3f,sizeof(f));
	int ans=0;
	f[1][0][0]=0,f[1][1][0]=-w[1];
	for(int i=2;i<=n;i++)
	{
		for(int j=0;j<=k;j++)
		{
			f[i][0][j]=f[i-1][0][j];
			if(j>=1) f[i][0][j]=max(f[i][0][j],f[i-1][1][j-1]+w[i]);
			f[i][1][j]=max(f[i-1][1][j],f[i-1][0][j]-w[i]);
			ans=max(ans,max(f[i][0][j],f[i][1][j]));
		}
	}
	printf("%d\n",ans);
	return 0;
}

4.股票交易(含冷冻期)

f[i][1]表示手中有票的最大利润,f[i][0]表示第i天手中无票第一天的最大利润,f[i][2]表示手中无票第二天及以后的最大利润

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100010
using namespace std;

int f[maxn][3];
int w[maxn];

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	f[1][0]=f[1][2]=0,f[1][1]=-w[1];
	for(int i=2;i<=n;i++)
	{
		f[i][0]=f[i-1][1]+w[i];
		f[i][1]=max(f[i-1][1],f[i-1][2]-w[i]);
		f[i][2]=max(f[i-1][2],f[i-1][0]);
	}
	printf("%d\n",max(f[n][0],f[n][2]));
	return 0;
}

posted on   dolires  阅读(8)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示