WEEK_4(DP初窥)

P3637 最长上升子序列

该题为动规板子题,主要思路:如果一个上升子序列的最大值小于它后面的一个数,那么这个子序列的长度就可以增加1,即将这个数纳入子序列,并保留以这个数结束的最优情况,比较以各个数结束的最优情况,用max取出最大值

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int a[N];
int main()
{
	int n;
	cin>>n;
	int dp[N];
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		dp[i]=1;
	}
	int maxx=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=i-1;j++)
		{
			if(a[i]>a[j])
			dp[i]=max(dp[j]+1,dp[i]);
		}
		maxx=max(dp[i],maxx);
	}
	cout<<maxx;
	return 0;
}

P1115 最大字段和

比较后一个数与前一个数之前的的最优情况加上这个数后的大小,若后一个数较大,则没有必要用前面的数之和加上这个数,以这个数开始即可,相反,后一个数相对较小,则加上这个数,并依次比较下去,期间用max一直记录最大值

还是题解说得明白:

  • 如果一个数加上上一个有效序列得到的结果比这个数大,那么该数也属于这个有效序列。
  • 如果一个数加上上一个有效序列得到的结果比这个数小,那么这个数单独成为一个新的有效序列

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int num[N],dp[N];
int main()
{
	int n;
	cin>>n;
	memset(num,0,sizeof(num));
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
	cin>>num[i];
	int maxx=-2147483647;
	for(int i=1;i<=n;i++)
	{
		dp[1]=num[1];
		dp[i]=max(dp[i-1]+num[i],num[i]);
		maxx=max(dp[i],maxx);
	}
	cout<<maxx;
	return 0;
}

P8707  [蓝桥杯 2020 省 AB1] 走方格

从题目来看,不难得出结论:第1行以及第1列的所有位置都可自成一种情况,故都赋上1

然后开始对横坐标都为偶数的点进行标注,即赋值为0

最后可以得到状态转移方程:dp[i][j]=dp[i-1][j]+dp[i][j-1]

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=35;
int dp[N][N];
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++)
	dp[i][1]=1,dp[1][j]=1;
	for(int i=2;i<=n;i++)
	for(int j=2;j<=m;j++)
	{
		if(i%2==0&&j%2==0)
		dp[i][j]=0;
		else
		dp[i][j]=dp[i-1][j]+dp[i][j-1];
	}
	cout<<dp[n][m];
	return 0;
}

P1216 数字三角形 Number Triangles

本题属于数塔模型,解决思路为:先递后归

将问题先分为各个小部分,首先从上往下分析可得,数之和若要取得最优解,直接受决定于这个数下面的左右子树的最优解,以此类推,问题最终解需要从数塔的最底层开始找,然后进行每轮比较,最终得出最大值

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
int dp[N][N];
int main()
{
	int r;
	cin>>r;
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=r;i++)
	for(int j=1;j<=i;j++)
	cin>>dp[i][j];
	int maxx=0;
	for(int i=r-1;i>=1;i--)
	{
		for(int j=1;j<=i;j++)
		{
			dp[i][j]+=max(dp[i+1][j],dp[i+1][j+1]);
			maxx=max(maxx,dp[i][j]);	
		}
	}
	cout<<maxx;
	return 0;
}

P1020 导弹拦截

这题真的ex,好好地o(n^{2})不行,非得o(n\log n)

这是o(n^{2})代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int main()
{
	int a[N],m=1,dp[N],ans=0;
	memset(dp,0,sizeof(dp));
	while(cin>>a[m])
	m++;
	m--;
	for(int i=1;i<=m;i++)
	{
		dp[i]=1;
		for(int j=1;j<=m;j++)
		{
			if(a[i]<=a[j]&&i!=j)
			dp[i]=max(dp[i],dp[j]+1);
		}
		ans=max(dp[i],ans);
	}
	cout<<ans<<endl;
	memset(dp,0,sizeof(dp));
	ans=0;
	for(int i=1;i<=m;i++)
	{
		dp[i]=1;
		for(int j=1;j<=m;j++)
		{
			if(a[i]>a[j]&&i!=j)
			dp[i]=max(dp[i],dp[j]+1);
		}
		ans=max(dp[i],ans);
	}
	cout<<ans;
  	return 0;
}

结果:

所以思前想后,如何利用二分查找解决此问题,是这样的:

第一个问题:

        此问题即找到一个最长非递增序列,那么可以先得到一个初始子序列,后一个数如果比最后一个数小,那么就接在这个数的后面,形成新一个子序列,但是如果比最后一个数大,就对其所在区间位置进行二分查找,并且替换掉较小数。(为了实现最长,则最后一个数越大就越有可能实现)一直进行下去直至最后,就可得到最终的最长非递增子序列

第二个问题:

        为了简化运算以及思路,引入Dilworth定理:

狄尔沃斯定理亦称偏序集分解定理,该定理断言:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真,它断言:对于任意有限偏序集,其最长链中元素的数目必等于其最小反链划分中反链的数目。

         在本题中,此定理的运用为:第二个问题可以转化成将第一个问题中的数反序排列,然后再求一遍最长非递增子序列,证明如下:

  • 当 n=1 时:命题显然成立。

  • 假设对于 n=k 结论成立,考虑 n=k+1 的情况:
    当 A 中最长链的长度为 k+1 时,令 M 为 A 中极大元的集合,显然 M 是一条反链。而且 A−M 中最长链的长度为 k。

由归纳假设,可以把 A−M 分成至少 k 个不相交的反链,加上反链 M,则 A 可分成至少 k+1 条反链。
因为 A 不可能分解成 n−1 条反链。假若只有 n−1 条反链,那么最长链的 n 个元素中必有 22 个元素被分到同一个反链,显然这与反链的定义矛盾。(摘自洛谷题解)

 代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int main()
{
	int m=1;
	int a[N];
	memset(a,0,sizeof(a));
	while(cin>>a[m])
	m++;
	m--;
	int d[N];
	memset(d,0,sizeof(d));
	int cou=1;
	d[1]=a[1];
	int xx=0;
	for(int i=2;i<=m;i++)
	{
		if(a[i]<=d[cou])
		{
			cou++;
			d[cou]=a[i];
		}
		else
		{
			int l=1,r=cou,mid=(r+l)>>1;
			while(l<r)
			{
				mid=(r+l)>>1;
				if(d[mid]<a[i])
				r=mid;
				else
				l=mid+1;
			}
			d[l]=a[i];
		}
	}
	cout<<cou<<endl;
	cou=1;
	reverse(a+1,a+1+m);
	memset(d,0,sizeof(d));
	d[1]=a[1];
	for(int i=2;i<=m;i++)
	{
		if(a[i]<d[cou])
		{
			cou++;
			d[cou]=a[i];
		}
		else
		{
			int l=1,r=cou,mid=(r+l)>>1;
			while(l<r)
			{
				mid=(r+l)>>1;
				if(d[mid]<=a[i])
				r=mid;
				else
				l=mid+1;
			}
			d[l]=a[i];
		}
	}
	cout<<cou;
	return 0;
}

以下是选做题:

AT_dp_f LCS

        最长公共子序列,英文缩写为LCS(Longest Common Subsequence)。定义为一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。

求最大长度:

        对于本题,要对字符串1和字符串2依次进行遍历,判断每个字符是否一样,每当有相同字符出现的时候,进行一次dp,这时的dp只与字符串1前一位和字符串2的前一位有关,由于要找到最长子序列,故要将该字符纳入子串,即:

dp[i][j]=dp[i-1][j-1]+1

        当遍历到不相同的字符时,则根据动规的最有子序列的特性,此次情况与字符串1上一个字符以及字符串2遍历上一个字符的情况有关,最优的话,则取二者最大值,即:

dp[i][j]=max(dp[i-1][j],dp[i][j-1])

求最大子序列:

        说真的这个挺难想的,想要找到具体的子序列,正向遍历字符串肯定不行,因为不能判断在哪一步dp有最大值,或者说是通往最大值的解,因此需要从最后开始,倒着向前遍历dp,由于正向遍历时,dp是一个一个遍历上去的,故倒过来也是一样,分别从字符串1和字符串2的最后一位开始,若字符相同,则存入另一数组中,若不同,则判断字符串1上一位与字符串2上一位的dp大小,最后取最大的

代码如下:

#include<bits/stdc++.h>
using namespace std;
int dp[3010][3010];
char s[3010],t[3010],ans[3010];
int main()
{
	scanf("%s%s",s+1,t+1);
	int n=strlen(s+1),m=strlen(t+1);
	for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++)
	if(s[i]==t[j])
	dp[i][j]=dp[i-1][j-1]+1;
	else
	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
	int i=n,j=m;
	while(dp[i][j]>0)
	{
		if(s[i]==t[j])
		ans[dp[i][j]]=s[i],i--,j--;
		else if(dp[i-1][j]>=dp[i][j-1])
		i--;
		else
		j--;
	}
	for(int i=1;i<=dp[n][m];i++)
	cout<<ans[i];
	return 0;
}

P2196 [NOIP1996 提高组] 挖地雷

看到这一题,想用爆搜,但还不会,qwq。。所以,用动规想了至少两天还没想出来,看题解也看不明白,谁懂啊,www

于是那天晚上发狠,弄不出来不睡觉了,终于,最后写出来了hhh

初始化:

        首先是要初始化,将每个隧道中地雷数赋给每一个dp[i],作为初始值,一开始挖地雷当然从1开始,故pos赋值1

找到地雷数最大值:

思路是这样的:大循环从1开始依次遍历到n,内循环从1开始遍历到大循环的 i ,每次以 i 结尾时找到考虑前面 i 个数的能挖到地雷数目加上第 i 个隧道中的地雷数的最大值,即:

if(mp[j][i]&&dp[j]+di[i]>dp[i])

dp[i]=dp[j]+di[i]

用maxx将每一次dp[i]储存,最后就可得到所挖地雷数的最大值了

挖到最大地雷数步骤:

        每当判断出来可以挖到第 i 个隧道中的地雷时,将此时的隧道记入数组中,每个下标对应数组的值,每次判断出来后就用 i 指向 j (可以看做逐渐建立子树),再判断此dp[i]是否可以是最大值(可以看成一种树结点,每次更换 i 可以看做更换一个父节点,然后将其枝丫接上去),并赋给pos,这样下去,逐渐就找到了每一次 i 所对应的最优解,最后也将最优子树连接起来,最后按照数组值指向对应下标,将下表倒序输出即可

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n;
const int N=35;
int mp[N][N];
int dp[N];
int di[N];
int so[N];
int pre[N];
int main()
{
	cin>>n;
	int pos;
	int maxx=0;
	memset(mp,0,sizeof(mp));
	memset(di,0,sizeof(di));
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
	cin>>di[i];
	for(int i=1;i<=n;i++)
	for(int j=i+1;j<=n;j++)
	cin>>mp[i][j];
	for(int i=1;i<=n;i++)
	dp[i]=di[i];
	memset(so,0,sizeof(so));
	maxx=di[1],pos=1;
	for(int i=2;i<=n;i++)
	for(int j=1;j<i;j++)
	{
		if(mp[j][i]&&dp[j]+di[i]>dp[i])
		{
			dp[i]=dp[j]+di[i];
			so[i]=j;
		}
		if(maxx<dp[i])
		{
			pos=i;
			maxx=dp[i];
		}
	}
	int cnt=0;
	while(so[pos]!=0)
	{
		pre[++cnt]=pos;
		pos=so[pos];
	}
	pre[++cnt]=pos;
	reverse(pre+1,pre+1+cnt);
	for(int i=1;i<=cnt;i++)
	{
		cout<<pre[i];
		if(i!=cnt)
		cout<<" ";
	}
	cout<<endl;	
	cout<<maxx;
	return 0;
}

P1541 [NOIP2010 提高组] 乌龟棋

本题觉得含金量不大,思路很好想,因有4个数,直接开四维数组即可,每一次循环找到对应的上一步棋的最优解,从而找到大问题最优解

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=41,M=125;
int n,m;
int grade[355];
int numm;
int num[5];
int step[N][N][N][N];
int a,b,c,d;
int main()
{
	memset(step,0,sizeof(step));
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	cin>>grade[i];
	for(int i=1;i<=m;i++)
	{
		cin>>numm;
		num[numm]++;
	}
	int r;
	step[0][0][0][0]=grade[1];
	for(int i=0;i<=num[1];i++)
	for(int j=0;j<=num[2];j++)
	for(int k=0;k<=num[3];k++)
	for(int l=0;l<=num[4];l++)
	{
		r=1+i+2*j+3*k+4*l;
		if(i!=0)
		step[i][j][k][l]=max(step[i-1][j][k][l]+grade[r],step[i][j][k][l]);
		if(j!=0)
		step[i][j][k][l]=max(step[i][j-1][k][l]+grade[r],step[i][j][k][l]);
		if(k!=0)
		step[i][j][k][l]=max(step[i][j][k-1][l]+grade[r],step[i][j][k][l]);
		if(l!=0)
		step[i][j][k][l]=max(step[i][j][k][l-1]+grade[r],step[i][j][k][l]);
	}
	cout<<step[num[1]][num[2]][num[3]][num[4]];
	return 0;
}

P2679 [NOIP2015 提高组] 子串

对于本题,四维数组挺好理解的,三维么,理解不了一点了。。。

四维数组其实很好想,分别对应四种状态

第一个状态,字符串A的每个字符

第二个状态,字符串B的每个字符

第三个状态,需要选出的子串数目

第四个状态,对此时字符串B的字符选或不选

但是这样会爆!!!

于是改用滚动数组,因为对于A的每个字符,存在选与不选两种情况(对应第四维的1和0),故只与上一个字符的最优解有关,则上面的方法需要存储的前 i-2种情况都可以删去,最终第 i 次只和第 i-1 次有关,所以

第一个状态,字符串A第 i 个字符与第 i-1个字符

可是要注意了,当A中有字符和B相等,此时对第 i 个字符有两种情况,即选与不选,选则有情况:纳入旧子串并使用前一个字符的,纳入新子串但不使用前一个字符,纳入新子串并使用前一字符,即:

dp[i%2][j][h][1]=dp[(i-1)%2][j-1][h-1][0]+dp[(i-1)%2][j-1][h][1])+dp[(i-1)%2][j-1][h-1][1];

若不相等了,则种数为前一字符所有情况之和,即:

dp[i%2][j][h][0]=dp[(i-1)%2][j][h][0]+dp[(i-1)%2][j][h][1];

P5322 [BJOI2019] 排兵布阵

这一题乍一看还是不太难的,再一看,是绿题,想着这也太不值了吧。。。

额额

可是这题让我知道,绿题是有它绿的原因的,太巧妙了hhh,这题别看它输入是一行行输士兵数,代表每个城堡对应的士兵数量,其实要把它倒过来看,把对每个城堡派去的士兵数看做一个集合,即对第 i 个城堡派去了5、3、4...进行这样一个巧妙地转换,就能使用背包算法了

最外面循环是对第 i 个城堡进行进攻,第二个循环是遍历攻打每个城堡的士兵数量,最里面一个循环,目的是遍历每个选手这样选择的情况数

当然了,作为背包问题,还是有一定模版的,只不过这题需要一个转的思想罢了,状态转移方程还是很友好的:

dp[j]=max(dp[j-2*stra[i][k]-1],dp[j])

代码如下:

#include<bits/stdc++.h>
using namespace std;
int s,n,m;
int stra[105][105];
int dp[20005];
int maxx=0;
int main()
{
	cin>>s>>n>>m;
	for(int i=1;i<=s;i++)
	for(int j=1;j<=n;j++)
	cin>>stra[j][i];
	for(int i=1;i<=n;i++)
	sort(stra[i]+1,stra[i]+1+s);
	for(int i=1;i<=n;i++)//第i个城堡 
	for(int j=m;j>=0;j--)//j个士兵 
	for(int k=1;k<=s;k++)//第k位选手 
	{
		if(j>2*stra[i][k])
		dp[j]=max(dp[j-2*stra[i][k]-1]+i*k,dp[j]);
	}
	cout<<dp[m];
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值