dp专题2--简单dp

最近的训练出现了好几个简单dp的题目,但是总是想不出状态和转移方程,甚至看到题解的状态和转移方程也写不出代码。所以,在此进行一些梳理。

状态的设计必须满足最优子结构,即必须要证明出:能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质。

(由于太弱了,本弱渣无法证明状态是否满足最优子结构...)

一、Hatsune Miku HDU - 5074 点击打开链接

题意:给你一个序列a[m],序列中的正数不能改变,负数能变成1~m的任意一个数,sum为(s[a[i]][a[i+1]])求和,题目求sum的最大值。

分析:题目要求出1~n的和最大,我们缩小范围求出1~i的和最大。因为i的状态与a[i-1]的值有关,所以状态为dp[i][j]表示前i个数中若a[i]为j所能得到的最大值。

            因为当原序列中a[i]为正数时,a[i]不能改变,所以我们将状态i与状态i+1分为下列4种状况:

            (1)a[i]<0&&a[i+1]<0   dp[i+1][k]=max(dp[i+1][k],dp[i][j]+s[j][k]);  k表示第i+1个数为k。

            (2)a[i]>0&&a[i+1]<0   dp[i+1][k]=max(dp[i+1][k],dp[i][a[i]]+s[a[i]][k]);

            (3)a[i]<0&&a[i+1]>0   dp[i+1][a[i+1]]=max(dp[i+1][a[i+1]],dp[i][j]+s[j][a[i+1]]);

            (4)a[i]>0&&a[i+1]>0   dp[i+1][a[i+1]]=max(dp[i+1][a[i+1]],dp[i][a[i]]+s[a[i]][a[i+1]]);

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=1e5+11;
const int inf=0x7fffffff;


int s[50][50];
int dp[100][100];
int a[100];
void work()
{
    memset(dp,0,sizeof(dp));
    memset(a,0,sizeof(a));
    memset(s,0,sizeof(s));
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&s[i][j]);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i0&&a[i+1]<0)
        {
            for(int k=1;k<=m;k++)
                dp[i+1][k]=max(dp[i+1][k],dp[i][a[i]]+s[a[i]][k]);
        }
        else if(a[i]<0&&a[i+1]>0)
        {
            for(int j=1;j<=m;j++)
                dp[i+1][a[i+1]]=max(dp[i+1][a[i+1]],dp[i][j]+s[j][a[i+1]]);
        }
        else if(a[i]>0&&a[i+1]>0)
            dp[i+1][a[i+1]]=max(dp[i+1][a[i+1]],dp[i][a[i]]+s[a[i]][a[i+1]]);
    }
    int ans=0;
    for(int i=1;i<=m;i++)
        ans=max(ans,dp[n][i]);
    printf("%d\n",ans);
}

int main()
{
#ifdef LOCAL
    freopen("input.in","r",stdin);
//    freopen("output.in","w",stdout);
#endif // LOCAL
    int T;
    while(~scanf("%d",&T))
    {
        while(T--)
            work();
    }
    return 0;
}

二、Happy Matt Friends HDU - 5119  点击打开链接

题意:给你n个数,从中选取一些数,使被选取数的异或和大于等于m,问有多少种方案。

分析:因为我们要求的是总方案数,且对于某个数我们有两种选择--选or不选,所以我们可以类比01背包问题,设计dp[i][j]表示前i个数异或和为j的方案数。

            因为a^b=c,,与a^a^b=a^c即b=c^a相等,且由题意可知第i个数我们可以选择--选or不选,所以转移方程为dp[i][j]=dp[i-1][j^a[i]]+dp[i-1][j]。

            dp[i-1][j^a[i]]表示选择第i个数且异或和为j^a[i]的总方案数,dp[i-1][j]表示不选择第i个数且异或和为j的总方案数。

            我们分析出了状态与转移方程,接下来我们考虑如何根据dp数组得到最终答案。因为dp[i][j]表示前i个数异或和为j的方案数,所以我们的最终的状态就是

            前n个数异或和为j的总方案数,即dp[n][j]表示最后不同异或和的相应总方案数,题目要求最后的异或和要大于等于m,所以我们只要把j>=m的dp[n][j]加起来

            就是答案。

            接下来我们考虑如何执行转移方程,因为i表示前i个数,因为当i=1时,1为第1个状态,我们不能从1的前一个状态计算出1的状态,所以要从2~n递推达到

            dp[n]的最终状态。因为前i个数不同的方案会产生不同的异或和,但是我们并不能确定不同方案的异或和,且异或和一定在0~((1<<21)-1)之间,

            所以我们可以枚举所有的可能值。

            最后,我们考虑如何初始化dp数组,从已知条件可知dp[i][a[i]]与dp[i][0]一定至少有一种方案,因为当i>1时,我们会从i-1的状态计算总方案数,

            如果我们初始化dp[i][0]=dp[i][a[i]]=1,则会出现重复,且我们从2递推到n,1的状态不能从前一个状态计算出。所以初始化dp[1][0]=dp[1][a[1]]=1。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=2100000;
const int inf=0x7fffffff;

int a[41];
int dp[41][maxn];
void work(int k)
{
    memset(a,0,sizeof(a));
    memset(dp,0,sizeof(dp));
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    dp[1][0]=dp[1][a[1]]=1;
    for(int i=1;i<=n-1;i++)
    {
        for(ll j=0;j<=maxn-2849;j++)
        {
            dp[i+1][j]=dp[i][j^a[i+1]]+dp[i][j];
        }
    }
    ll ans=0;
    for(int i=m;i<=maxn-2849;i++)
        ans+=(1LL*dp[n][i]);
    printf("Case #%d: %lld\n",k,ans);
}

int main()
{
#ifdef LOCAL
    freopen("input.in","r",stdin);
//    freopen("output.in","w",stdout);
#endif // LOCAL
    int T;
    while(~scanf("%d",&T))
    {
        int k=1;
        while(T--)
        {
            work(k);
            k++;
        }
    }
    return 0;
}

三、Dire Wolf HDU - 5115  点击打开链接

题意:有n只狼消灭一只狼会获得a[i]的伤害和相邻两只狼的b[u]+b[v]的伤害,问消灭所有的狼的最小伤害。

分析:根据题意我们要求的是消灭1到n的狼的最小伤害,我们可以缩小所求范围求消灭i到j的狼的最小伤害,所以我们可以设计状态为dp[i][j]表示消灭i到j的狼的最小伤害。

            接着我们考虑:

            当范围缩小到最小时i==j时,dp[i][j]=a[i]+b[i-1]+b[j+1],

            当范围缩小到i==j-1时,dp[i][j]=min(a[i]+b[i-1]+b[j]+a[j]+b[i-1]+b[j+1],a[j]+b[i]+b[j+1]+a[i]+b[i-1]+b[j+1]),

            当范围缩小到i==j-2时,我们可以利用上面求得的结果来求消灭i到j的狼的最小伤害。若第i个狼是i~j最后一个被消灭的话,我们只需求

            a[i]+b[i-1]+b[j+1]+dp[i+1][j],若第i+1个狼是i~j最后一个被消灭的话,我们只需求a[i+1]+b[i-1]+b[j+1]+dp[i][i]+dp[j][j]....

            类比,得出转移方程dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+a[k]+b[i-1]+b[j+1]),k表示第k个狼是i~j最后一个被消灭的。

            从转移方程我们可以知道我们是通过两段长度比i~j小的部分求出dp[i][j],所以我们必须先求出j-i小的再求j-i大的。

            因为i==j是第一种状态,所以我们必须先初始化dp[i][j]=a[i]+b[i-1]+b[j+1]。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=210;
const int inf=0x7fffffff;

int a[maxn],b[maxn];
int dp[maxn][maxn];
void work(int k)
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
        scanf("%d",&b[i]);
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;i++)
    {
        for(int j=i;j<=n;j++)
        {
            if(i==j) dp[i][j]=a[i]+b[i-1]+b[j+1];
            else
            dp[i][j]=inf;
        }
    }
    for(int i=1;i

总结:

我们要考虑状态、转移方程、怎样枚举以及怎样初始化。

从这三题看来,状态是将题目的所求范围/规模缩小(但可能有时不能满足最优子结构),状态的结果就是缩小后的结果,然后考虑从一个状态转移到另一个状态需要储存什么信息,如果有此类信息就增加维度来记录下这些信息。

转移方程需要根据状态来考虑。

枚举根据转移方程枚举,有时从1~n枚举,有时按j-i从小到大枚举。

从这三道题看来,初始化是将第一状态先求出来,再使用转移方程。

你可能感兴趣的:(dp,总结)