学习记录-环上dp的两种方法(以poj2228和CH5501为例)

环上dp的两种方法

第一种:
先考虑无环的情况,再考虑有环的情况。
这种方法一般需要两边线性dp。第一遍的初始条件时无环情况。第二遍的初始条件是有环条件。
第二种方法:
把无环的情况复制一遍到数组的末尾 ,然后在对这个数组进行线性处理,这时就包含了有环时的情况。在dp中,这种方法一般需要数据结构来优化才能成为线性的时间复杂度。
下面通过两个例题来说明。

例题:

第一种方法:分别考虑有环和无环的两种情况
例如poj2228
链接:http://poj.org/problem?id=2228
1.题面:
给定一个长度为n的数组,并且首尾相连形成一个环。在这个环中选择b个数(不一定连续),求选择的b个数的权值和的最大值。权值和有如下规定:选择单个的不连续的数是没有权值的,只要选择连续的两个数或者两个数以上的数,权值才能加上除了第一个数之外的数的权值。就相当于每选择一段连续的数,选择的第一个数是为了给增加权值做准备,这一段数中第一个数后面的数的权值才能加到答案里面。比如:从 1 2 3 4 5五个数里面选择b=3个数,那么如果选择1,2,3,那么权值就是2+3=5;如果选择1,3,5,那么权值就是0。
数据量:n<=4e4,b<=n。
2.解题思路:
可以用三维的dp来表示每一个状态
d[i][j][1]表示目前到第i个数,已经选择了j个数,并且第i个数选择了,这时的最大权值
d[i][j][0]表示目前到第i个数,已经选择了j个数,并且第i个数没有选择,这时的最大权值
那么就有递推公式
d[i][j][0]=max(d[i-1][j][0],d[i-1][j][1]);
d[i][j][1]=max(d[i-1][j-1][0],d[i-1][j-1][1]+a[i]);
在考虑两种不同的情况:
首先没有连续选择最后一个数和第一个数:
那么就和无环情况一样,初始化memset(d,-INF,sizeof(d));然后d[1][0][0]=d[1][1][1]=0即可。
目标状态是max(d[n][b][1],d[n][b][0]);
在考虑最后一个数和第一个都选择了的情况:
那么就有初始化状态为d[1][1][1]=a[1];然后继续向后递推。
目标状态是d[n][b][1];
最后维护最大值ans即可。
还有一点是这个题卡内存,所以由于每一维都是由前一维推出来,第一维用滚动数组就行了。
AC代码

//#include<bits/stdc++.h>
#include<algorithm>
#include<complex>
#include<iostream>
#include<iomanip>
#include<ostream>
#include<cstring>
#include<string.h>
#include<string>
#include<cstdio>
#include<cctype>
#include<vector>
#include<cmath>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cstdlib>
#include<time.h>
#include<ctime>
#include<bitset>
#define pb push_back
#define _fileout freopen("out.txt","w",stdout)
#define _filein freopen("in.txt","r",stdin)
#define ok(i) printf("ok%d\n",i)
using namespace std;
typedef double db;
typedef long long ll;
const double PI = acos(-1.0);
const ll MOD=1e9+7;
const ll NEG=1e9+6;
const int MAXN=4e3;
const int INF=0x3f3f3f3f;
const ll ll_INF=9223372036854775807;
const double eps=1e-8;
ll qm(ll a,ll b){ll ret = 1;while(b){if (b&1)ret=ret*a%MOD;a=a*a%MOD;b>>=1;}return ret;}
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
ll lcm(ll a,ll b){return (a*b)/gcd(a,b);}
int n,b;
int a[MAXN];
int d[3][MAXN][2];//d[i][j][k]表示目前前i个小时睡了j小时,若k==1,则第i个小时没睡,否则第i个小时睡了。
int main()
{
    scanf("%d%d",&n,&b);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i+n]=a[i];
    }
    memset(d,-INF,sizeof(d));
    d[1&1][0][0]=0;
    d[1&1][1][1]=0;
    for(int i=2;i<=n;i++)
    {
        for(int j=0;j<=b;j++)
        {
            d[i&1][j][0]=max(d[(i-1)&1][j][0],d[(i-1)&1][j][1]);
            if(j>0)d[i&1][j][1]=max(d[(i-1)&1][j-1][0],d[(i-1)&1][j-1][1]+a[i]);
            // printf("d[%d][%d][0]=%d d[%d][%d][1]=%d\n",i,j,d[i&1][j][0],i,j,d[i&1][j][1]);
        }
    }
    int ans=max(d[n&1][b][0],d[n&1][b][1]);
    // exit(1);
    memset(d,-INF,sizeof(d));
    d[1][1][1]=a[1];
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=b;j++)
        {
            d[i&1][j][0]=max(d[(i-1)&1][j][0],d[(i-1)&1][j][1]);
            if(j>0)d[i&1][j][1]=max(d[(i-1)&1][j-1][0],d[(i-1)&1][j-1][1]+a[i]);
             // printf("d[%d][%d][0]=%d d[%d][%d][1]=%d\n",i,j,d[i&1][j][0],i,j,d[i&1][j][1]);
        }
    }
    ans=max(ans,d[n&1][b][1]);
    printf("%d",ans);

    return 0;
}

同样我觉得这个题可以用第二种方法,复制一段数组到末尾,然后初始化d数组都为0,然后第二层循环j从2开始递推,因为j=1的时候,只能准备加权值,所以一定是0;但是这种方法不知为什么WA了,找反例找不出来,只能暂时先存这儿吧!
不知道为什么会wa的方法(wa代码)

//#include<bits/stdc++.h>
#include<algorithm>
#include<complex>
#include<iostream>
#include<iomanip>
#include<ostream>
#include<cstring>
#include<string.h>
#include<string>
#include<cstdio>
#include<cctype>
#include<vector>
#include<cmath>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cstdlib>
#include<time.h>
#include<ctime>
#include<bitset>
#define pb push_back
#define _fileout freopen("out.txt","w",stdout)
#define _filein freopen("in.txt","r",stdin)
#define ok(i) printf("ok%d\n",i)
using namespace std;
typedef double db;
typedef long long ll;
const double PI = acos(-1.0);
const ll MOD=1e9+7;
const ll NEG=1e9+6;
const int MAXN=4e3;
const int INF=0x3f3f3f3f;
const ll ll_INF=9223372036854775807;
const double eps=1e-8;
ll qm(ll a,ll b){ll ret = 1;while(b){if (b&1)ret=ret*a%MOD;a=a*a%MOD;b>>=1;}return ret;}
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
ll lcm(ll a,ll b){return (a*b)/gcd(a,b);}
int n,b;
int a[MAXN*2];
int d[3][MAXN][2];//d[i][j][k]表示目前前i个小时睡了j小时,若k==1,则第i个小时没睡,否则第i个小时睡了。
int main()
{
    scanf("%d%d",&n,&b);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i+n]=a[i];
    }
    memset(d,0,sizeof(d));
    d[1&1][0][0]=0;
    d[1&1][1][1]=0;
    for(int i=2;i<=2*n;i++)
    {
        for(int j=2;j<=b;j++)
        {
            d[i&1][j][0]=max(d[(i-1)&1][j][0],d[(i-1)&1][j][1]);
            if(j>0)d[i&1][j][1]=max(d[(i-1)&1][j-1][0],d[(i-1)&1][j-1][1]+a[i]);
            // printf("d[%d][%d][0]=%d d[%d][%d][1]=%d\n",i,j,d[i&1][j][0],i,j,d[i&1][j][1]);
        }
    }
    int ans=max(d[(2*n)&1][b][0],d[(2*n)&1][b][1]);
    printf("%d",ans);
    return 0;
}

第二种方法:复制一遍到数组末尾
CH5501:
链接:http://contest-hunter.org:83/contest/0x50「动态规划」例题/5501 环路运输
1:题意:
一个数组a[n]首尾相连,每两个元素之间有一个距离,定义为dist[i,j]=min(abs(i-j),n-abs(i-j));即顺时针或者逆时针从i到j 的最短路径。求a[i]+a[j]+dist[i,j]的最大值。
数据量:n<=1e6。
2.思路
首先考虑暴力,即暴力找i和j,时间复杂度是O(N^2)。明显超时。
可以把a[n]赋值一段到a的末尾。然后对2n的数组进行线性操作。考虑到i和j,如果i-j<=n/2,那么dist(i,j)就等于i-j;如果i-j>n/2,那么就对应成了i,j+n。权值为a[i]+a[j+n]+dist[i][j+n]。
综上所述,原问题可以转化为在一个2n的数组中,寻找1<=j<i<=n并且i-j<=n/2的情况下,a[i]+a[j]+dist(i,j)的最大值。即a[i]+a[j]+i-j的最大值。
所以从前往后遍历,每一个i,a[i]+i是可以直接得出的,然后只需要找出在i-j<=n/2的情况下,a[j]-j的最大值。可以用双端队列进行优化,使每次找j的代价为o(1),所以总的时间复杂度就是O(N)。
双端队列优化思路:考虑到 k<j 并且 j 和 k 都符合大于 等于i-n/2 小于 i 这一个条件。如果 a[j]-j 的值大于 a[k]-k 的值,那么可以发现对于 i 和 i 之后的数,肯定不会选择 k 这个数,因为 j 不仅权值大于k,而且相对于 k 靠后。
所以双端队列就可以按照权值从前往后逐渐减小存下标,按照先后顺序有三个操作:
1:如果目前第一个数小于 i-n/2 那么就把第一个数删除
2:如果目前的一个数大于等于 i-n/2 ,那么双端队列的第一个数就是目前第 i 位对应的权值最大的j。
3:如果目前的双端队列的末尾的数小于a[i]-i ,那么就把双端队列最后一个数删除,直到双端队列最后一个数大于a[i]-i为止。然后把a[i]-i添加到双端队列的末尾。
代码

//#include<bits/stdc++.h>
#include<algorithm>
#include<complex>
#include<iostream>
#include<iomanip>
#include<ostream>
#include<cstring>
#include<string.h>
#include<string>
#include<cstdio>
#include<cctype>
#include<vector>
#include<cmath>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cstdlib>
#include<time.h>
#include<ctime>
#include<bitset>
#define pb push_back
#define _fileout freopen("out.txt","w",stdout)
#define _filein freopen("in.txt","r",stdin)
#define ok(i) printf("ok%d\n",i)
using namespace std;
typedef double db;
typedef long long ll;
const double PI = acos(-1.0);
const ll MOD=1e9+7;
const ll NEG=1e9+6;
const int MAXN=2e6+10;
const int INF=0x3f3f3f3f;
const ll ll_INF=9223372036854775807;
const double eps=1e-8;
ll qm(ll a,ll b){ll ret = 1;while(b){if (b&1)ret=ret*a%MOD;a=a*a%MOD;b>>=1;}return ret;}
ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
ll lcm(ll a,ll b){return (a*b)/gcd(a,b);}
int n;
int a[MAXN];
int b[MAXN];
int ans;
deque<int>q(1,0);
void show(int *c)
{
    for(int i=1;i<=n;i++)
        printf("%d ",c[i]);
    printf("\n");
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i+n]=a[i];
    }
    for(int i=1;i<=2*n;i++)
    {
        b[i]=a[i]-i;
    }
    // show(a);
    // show(b);
    if(n==1)
    {
        printf("%d\n",a[1]);return 0;
    }
    for(int i=1;i<=2*n;i++)
    {
        while(!q.empty()&&q.front()<i-n/2)q.pop_front();
        ans=max(ans,a[i]+i+b[q.front()]);
        while(!q.empty()&&b[i]>=b[q.back()])q.pop_back();
        q.push_back(i);
        // printf("i=%d ans=%d q.front=%d\n",i,ans,q.front());
    }
    printf("%d\n",ans);
    return 0;
}

总结:

环形数组在题目中很常见,一般较为简单的解决方法是赋值一边倒数组末尾,然后线性解决。
但是初始化条件很关键,数组从哪一位开始递推也同样关键。但是不管怎样,最重要是还是递推式!
dp题目,递推式出来,基本上题目就已经完成90%了。
dp中很多时候是需要优化的,比如数据结构优化,STL优化,不过优化的一般方法都是在决策集合中及时排除一定不是最优解的选择,并及时更新最优解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值