最近的训练出现了好几个简单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;
}
题意:给你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;
}
题意:有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从小到大枚举。
从这三道题看来,初始化是将第一状态先求出来,再使用转移方程。