动态规划一

复健\(Day4\)

动态规划(一)线性\(DP\)

\(1.\)数字三角形模型

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

一般的解法都是设状态为\(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\)个单位时间。

商人必须在\((2N−1)\)个单位时间穿越出去。

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

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

请问至少需要多少费用?

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

输入格式:

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

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

输出格式:

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

数据范围:

\(1≤N≤100\)

注意到我们需要在\(2N-1\)的时间内出去,实际也就是意味着我们只能往下或者往右跑,与上一题思路相同,设状态为\(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\)上的二元关系\(\leqslant\)满足以下三个条件则称其为\(P\)上的偏序关系

\(1.\)自反性:\(a\leqslant a\)\(\forall a\in P\)

\(2.\)反对称性:\(\forall a,b \in P\),若\(a\leqslant b\)\(b\leqslant a\),则\(a=b\)

\(3.\)传递性:\(\forall a,b,c\in P\),若\(a\leqslant b\)\(b\leqslant c\),则\(a\leqslant c\)

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

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

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

#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[i-1][0],f[i-1][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 2023-08-03 14:41  dolires  阅读(27)  评论(0)    收藏  举报

导航