一、状态设计优化:
暴力状态设计:dp[i][j][k][l][x]表示前四种卡片分别用了i,j,k,l张,目前走到第x个格子,以用了第三张为例,状态转移方程为
dp[i][j][k][l][x]=max(dp[i][j][k][l][x],dp[i][j][k-1][l][x-3]+w[x]);
优化:容易发现x=i+2j+3k+4l+1,x那一维可以被优化掉,状态转移方程为
dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k-1][l]+w[x]);
暴力状态设计:dp[i][j][k]表示在前i个里选,左端重量为j,右端重量为k,需要开dp[100][10000][10000]的数组,明显不可行。
优化:注意到只需要考虑差值即可,用dp[i][j]表示考虑前i个物品,天平重量差为j,枚举两种选择:在重端放物品 和 在轻端放物品,状态转移方程为
dp[i][j]=max({dp[i-1][j],dp[i-1][abs(j-a[i])]+a[i],dp[i-1][j+a[i]]+a[i]});
二、贪心:
如果直接用背包做会导致答案错误,因为背包选择的物品是无序的,但是这题的价值会随着时间而减小,需要先贪心地进行排序,再进行01背包。
struct node{
ll b,a,c;
}a[1000010];
bool cmp(node x,node y)
{
return x.c*y.b<y.c*x.b;
}
[NOIP2007]守望者的逃离 (nowcoder.com)
很容易想到用dp[i][j]表示经过时间i时剩余的魔法值为j,但是总魔法值有1e8之大。所以用贪心的思路来想,只要魔法值足够就开始瞬移来尽可能快地逃离。
注:贪心需慎重
三、滚动数组优化:
第一次遇见的就是在01背包一维优化的时候。如果dp[i][j]都是由dp[i-1][j]转移而来时,同时i的范围高达1e5,那么可以用滚动数组反复覆盖i-1的状态,只需要开dp[2][j]的数组即可,用dp[i&1][j]表示i的状态,dp[i&1^1][j]表示i-1的状态,初始化的时候需要写在i的内部。
for(int i=1;i<=n;i++) dp[i&1][0]=1;
本题转移方程简单,但是需要对时间这一维做滚动数组。
dp[0][1][1][4]=1;
for(int T=1;T<=t;T++)
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int d=0;d<5;d++)
{
int k=i+dx[d],l=j+dy[d];
if(k<1||k>n||l<1||l>m) continue;
ll sum=0;
if(max(abs(x1-k),abs(y1-l))<=D)
{
if(d==4)
for(int idx=0;idx<=4;idx++) sum=(sum+dp[T&1^1][k][l][idx])%mod;
else sum=(sum+dp[T&1^1][k][l][d]+dp[T&1^1][k][l][4])%mod;
}
else
for(int idx=0;idx<=4;idx++) sum=(sum+dp[T&1^1][k][l][idx])%mod;
dp[T&1][i][j][d]=sum%mod;
}
ll ans=0;
for(int i=0;i<=4;i++) ans=(ans+dp[t&1][x2][y2][i])%mod;
四、区间分割:
经典的将区间分割成几部分,然后求总体价值。枚举分割次数、当前分割点和上一个分割点即可。
for(int i=1;i<=n;i++) dp[0][i]=calc(1,i);
for(ll i=1;i<=k;i++)
for(ll j=i+1;j<=n;j++)
for(ll l=i;l<j;l++) dp[i][j]=max(dp[i][j],dp[i-1][l]*calc(l+1,j));
将区间分割成几部分,但是有一些部分可以不选,设dp[i][j][k][1/0]表示A匹配到第i位,B匹配到第j位,选了k个子串,A的第i位选或不选的状态。那么状态的可能情况为:
第i位不选,i-1位选或不选都可以,转移方程为
dp[i&1][j][k][0]=(dp[i&1^1][j][k][0]+dp[i&1^1][j][k][1])%mod;
只有A[i]==B[j]第i位才能选。上一位如果不选,这一位就相当于新开了一个子串;如果上一位选了,可以将上一位与这一位看成是一个子串,也可以看成是相邻的两个子串,转移方程为
dp[i&1][j][k][1]=(dp[i&1^1][j-1][k][1]+dp[i&1^1][j-1][k-1][0]+dp[i&1^1][j-1][k-1][1])%mod;
本题重点是对这个表达式进行分析,设dp[i][j][0/1/2]表示A遍历到第i位,B遍历到第j位,没选空格/在A上选了空格/在B上选了空格的最大相似度。如果上一次没选空格,这次在某个串上选了空格,那么答案应该是上一状态-A;如果两次都在同一个串上选了空格,那么答案应该是上一状态-B。
dp[0][0][0]=0,dp[1][0][2]=-A,dp[0][1][1]=-A;
for(int i=2;i<=n;i++) dp[i][0][2]=dp[i-1][0][2]-B;
for(int i=2;i<=m;i++) dp[0][i][1]=dp[0][i-1][1]-B;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=0;k<=2;k++)//0没选空格,1在A选空格,2在B选空格
for(int l=0;l<=2;l++)
if(l==0) dp[i][j][l]=max(dp[i][j][l],dp[i-1][j-1][k]+w[a[i]-'A'][b[j]-'A']);
else if(l==1)
if(k==0||k==2) dp[i][j][l]=max(dp[i][j][l],dp[i][j-1][k]-A);
else dp[i][j][l]=max(dp[i][j][l],dp[i][j-1][k]-B);
else
if(k==0||k==1) dp[i][j][l]=max(dp[i][j][l],dp[i-1][j][k]-A);
else dp[i][j][l]=max(dp[i][j][l],dp[i-1][j][k]-B);
五、背包dp:
[NOIP2014]飞扬的小鸟 (nowcoder.com)
01背包与完全背包的综合,重点在完全背包部分。设dp[i][j]为小鸟飞到横坐标i,纵坐标j的位置最小点击次数。由于上升可以无限点击,因此是个完全背包,二维的完全背包比01背包多了一种状态,就是需要自己更新自己。同时,由于限制最高坐标为m,所以完全背包的容量其实是m加上一次点击的上升高度up[i],最后将dp[i][m]~dp[i][m+up[i]]的结果遍历更新dp[i][m]即可。
for(int j=1;j<=m+up[i];j++) dp[i][j]=min({dp[i][j],dp[i-1][j-up[i]]+1,dp[i][j-up[i]]+1});
for(int j=m+1;j<=m+up[i];j++) dp[i][m]=min(dp[i][j],dp[i][m]);
六、数据结构优化状态计算:
类似于乘积最大这道题,每次转移都需要求出[l+1,j]这段区间的和,因此可以用前缀和预处理。
[NOIP2001]统计单词个数 (nowcoder.com)
转移方程依然类似于乘积最大,但是要计算区间内出现单词个数,需要用字典树优化。
int query(int l,int r)
{
int num=0;
for(int i=l;i<=r;i++)
{
int p=0;
for(int j=i;j<=r;j++)
{
int u=s[j]-'a';
if(!trie[p][u]) break;
p=trie[p][u];
if(cnt[p])
{
num++;
break;
}
}
}
return num;
}
Cashback - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
得出转移方程后需要求固定区间长度的最值,需要用单调队列或者线段树优化。
七、前缀和优化:
相当重要的一种优化。对于这种转移方程,看似是O(n^3)的枚举,但是注意到每次都要用到上一轮状态第二维遍历的总和,因此每次遍历第二维后可以将总和先记录下来,以便遍历下一个i时使用,时间复杂度可以降到O(n^2)。
注意到xi的范围很大,同时数组元素之间只有大小关系是有用的,因此可以离散化。用b[i]表示小于等于i的数的个数,f[i][j]表示合法的长度为i且第i个数为j的序列个数,状态转移方程为
发现可以用前缀和进行优化,用dp[i][j]辅助记录前缀和,代码如下:
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(i==1) f[i][j]=1;
else if(i<=b[j]) f[i][j]=dp[i-1][j];
else f[i][j]=0;
ans[i]=(ans[i]+f[i][j])%mod;
dp[i][j]=(dp[i][j-1]+f[i][j])%mod;
}
}
八、状态压缩dp:
1、最常见的一种是在二维表格上涂色。
Corn Fields (nowcoder.com) 玉米田,经典的简单状压dp(第一道应该是蒙德里安的梦想)
经过数学分析之后,发现对于每个数来说就是在构建一个二维表格A,A[i][j]=A[i][j-1]*3,A[i][j]=A[i-1][j]*2。初始状态A所有格子都是白色,现在对一些部分涂黑,问有多少种涂色方案保证没有两个涂黑的格子相邻。这样的话,这题就相当于不规则图形版的玉米田。
从小到大遍历集合中的数,每次遍历线性筛去它的倍数,并构建表格进行状压dp。
ll solve(int x)
{
n=1;
for(int i=1;i<=17;i++) len[i]=0;
while((x<<(n-1))<=num)
{
p=x<<(n-1);
st[p]=1;//筛去倍数
while(p<=num) st[p]=1,len[n]++,p*=3;
n++;
}
n--;
for(int i=1;i<=n+1;i++)
for(int j=0;j<1<<len[i];j++) dp[i][j]=0;
dp[0][0]=1;
for(int i=1;i<=n+1;i++)
for(int k=0;k<1<<len[i-1];k++)
if(dp[i-1][k])
for(int j=0;j<1<<len[i];j++)
if((!(j&(j<<1)))&&!(j&k))
dp[i][j]=(dp[i][j]+dp[i-1][k])%mod;
return dp[n+1][0];
}
发现列数高达1e18,但是行数范围很小。设f[i][j]表示枚举到第i行状态为j时的方案数,则转移方程为(当然这个和前缀和没啥关系)。线性递推是行不通的,一种常用的优化线性递推的方法为矩阵快速幂:
用转移矩阵进行状态转移,
最终答案为
for(ll i=0;i<(1<<m);i++)
for(ll j=0;j<(1<<m);j++)
if((i&j)==0&&(i|j)) tran[i][j]=1;//状态转移矩阵,白色为1,黑色为0
for(ll i=0;i<(1<<m);i++) f[i][i]=1;
ll k=n-1;
while(k)//矩阵乘法
{
if(k&1) mul(f,f,tran);
k>>=1;
mul(tran,tran,tran);
}
for(ll i=0;i<(1<<m);i++)
for(ll j=0;j<(1<<m);j++) ans=(ans+f[i][j])%mod;
2、还有一种常见的图论上的状压dp
用二进制串表示各个目标点是否已经到达,则用dp[i][j]表示当前状态为i,最后一个到达的点为j时的最小花费,则状态转移方程为
其中需要用Floyd算法预处理,dp部分代码如下:
for(int i=0;i<(1<<R);i++)
for(int j=0;j<R;j++)
if(i&(1<<j))
for(int k=0;k<R;k++)
dp[i+(1<<k)][k]=min(dp[i+(1<<k)][k],dp[i][j]+g[r[j]][r[k]]);
九、概率dp:
先求出每台发动机启动成功的概率b[i],用dp[i][j]表示在前i个发动机中有j台被启动成功,状态转移方程显然为
(以后还会回来更新的)