入门dp——初学 ==\/==

所谓的dp,就是动态规划,也就是必须“动态”的,个人的理解是有规律地访问之前的状态,来更新下一步状态。常见的dp有很多种:背包,树形dp,数位dp……这时请看标题,这里是入门dp!巨佬的话请往其他地方走~~~

最简单有最直白的dp,就是斐波那契数列了吧!f[i]=f[i-1]+f[i-2],这是人尽皆知的过程了。需要记住的是:当前推导的状态不能平白无故出现,除初始化之外,剩下的状态只能通过几种特定的状态来得到。

之后还有一种非常简单的dp名曰:(最短路)Floyd。这里有核心的代码片段:

for(int k=1;k<=n;k++)//枚举我从i走到j需要路过的中间点
{
    for(int i=1;i<=n;i++)//枚举起点
    {
        for(int j=1;j<=n;j++)//枚举终点
        {
            f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
        }
    }
}

记住,要把k放在最外面,不然会结果偏大。为什么?我思索半天,得出了一个结论:

当先枚举起点和终点时,当起点和终点最短路要经过至少两个点的时候。即使f[i][k](或f[k][j])的状态是最短路,也并不能保证另一条作为起点和终点一定被访问过。也就是说,我们无法通过两条路来凑成f[i][j]的最短路。又因为i和j放在最外面,当for完最里面的循环之后f[i][j]没有被再次改变的可能,所以是错误的。

之后也有个问题,为什么将k放在外面是成立的呢?如果存在经过k点的最短路,那至少一条路径(f[i][j])只经过k点。而剩下的关于k点的最短路径都是由这条路径拼接而成的。对于一条有k1,k2,k3,k4这些中间点的最短路径,先将它铺平(图难看,线段表示已知的最短路径):

i          k1         k2          k3         k4          j

———   ———   ———  ———  ——— 

假设我先访问到的k是k2,那么f[k1][k3]即使最短路径,则

i          k1         k2          k3         k4          j

———   ———————  ———  ———

之后假设访问到的是k4那么f[k3][j]是最短路径,则 

i          k1         k2          k3         k4          j

———   ———————  ———————

然后访问到k3作为中间点,则

i          k1         k2          k3         k4          j

———   ——————————————

最后访问到k1的时候就能保证路径最短啦!

还有呢,以为就这点内容?(其实已经1000+字了)

有一种非常神奇却简单,对于像我这样的人却很难想得到的dp思路。这里参考我在51nod上做的一题最大M子段和,下面是我的做法,不是最优,而是个人的解法:

大概的意思是,在一个有正有负的数列中,选出M个子段,是其子段的和最大。

有正有负?这让我想到了什么,于是我第一步想做的事是合并连续的同符号的数。于是最后序列会变成相邻两个子段符号都不相同的样子。之后我们再进行选择和合并,就找到了最大的子段和了。

之后我设置了一个dp的二维数组。从数列的头部开始,dp[ j ][ 1 ]表示目前我访问到当前这个正子段时,我前面一共选了j个子段,而且这一个正子段的前一个正子段选了; dp[ j ][ 0 ]表示目前我访问到当前这个正子段时,我前面一共选了j个子段,而且这一个正子段的前一个正子段没有选

对于每一个正子段,我们可以有两种选择,选和不选;选择的情况还分为两种,和上一个合并,单独成为一个新子段。于是就有如下代码片段:

dp[j][0]=max(dp[j][1],dp[j][0]);
dp[j][1]=max(dp[j-1][1]+f[i],max(dp[j][1]+f[i-1]+f[i],dp[j-1][0]+f[i]));
//f[i]表示的是当前正子段的值

后面这是一份完整的代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 5001
#define LL long long
using namespace std;
LL f[maxn],tot,m,n,cnt,num,dp[maxn][2];//cnt是子段个数,num是正子段个数 
int main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		LL x;
		scanf("%lld",&x);
		if(x>0)
		{
			tot+=x;
			if(f[cnt]>0)f[cnt]+=x;
			else{f[++cnt]+=x;num++;}
		}
		else if(x<0)
		{
			if(f[cnt]<0)f[cnt]+=x;
			else f[++cnt]+=x;
		}
	}//f存子段就没什么好解释了吧,0可以直接跳过 (默认和周围合并了 
	if(m>=num){printf("%lld",tot);return 0;}
	//题目是当 m不小于正数个数是就输出正整数累加值,实际上不小于正数子段个数时就可以直接输出了 
	if(f[1]>0)dp[1][1]=f[1];else dp[1][1]=f[2];//初始化f[1]和f[2]有且只有一个正子段 
	for(int i=3;i<=cnt;i++)
	{
		if(f[i]>0)
		{
			for(int j=m;j>0;j--)//必须从m-1开始倒着推,避免有一种叫做后效性的东西影响 
			{ 
				dp[j][0]=max(dp[j][1],dp[j][0]);//避免后效性,先修改这一条 
				dp[j][1]=max(dp[j-1][1]+f[i],max(dp[j][1]+f[i-1]+f[i],dp[j-1][0]+f[i]));
				//f[i]表示的是当前正子段的值
			}
		}
	} 
	printf("%lld",max(dp[m][1],dp[m][0]));//最后一个数可以选可以不选 
	return 0;
}

这篇很长,看完真的不容易,谢谢大家。(虽然不感觉会有人看)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值