例题1:最长上升子序列,每两个之间不能超过d;
思路:
f[i]表示以i结尾的满足条件的子序列最大长度。
f[i]可以从值在[ai-d,ai-1]范围内的位置转移过来。 相当于是区间最大值。
可以用线段树维护,a[i]-d<=a[j]<a[i],每判断一次就维护一遍线段树。
线段树、平衡树等数据结构可以优化一些较有 规律(如区间最值)的转移。
例题2:HH有个一成不变的习惯,喜欢饭后百步走。所 谓百步走,就是散步,就是在一定的时间内,走过 一定的距离。但是同时HH又是个喜欢变化的人,所 以他不会立刻沿着刚刚走来的路走回。又因为HH是 个喜欢变化的人,所以他每天走过的路径都不完全 一样,他想知道他究竟有多少种散步的方法。现在 给你学校的地图,有n个点m条边,问长度为t,从给 定地点A走到给定地点B共有多少条符合条件的路径。
N ≤ 20,M ≤ 60,t ≤ 2^30
思路:f[i] [j] [e]表示从起点出发走了i步,且这i步不是回头的i步(是合法的),走到了j,e为刚来的路,有多少方案。
f[i] [j] [e] <—f[i-1] [k] [e'] <e连接了j和k,枚举e‘,e'!=e>
f[0] [s] [e_0]=1;e_0代表一条虚边,若是1-m的任意一点会冲突,只能用不是图里的点。
优化:从题目给的起点出发,f[i] [e] [r:0/1]表示从起点出发走了i步,且这i步不是回头的i步(是合法的),走到了j,e为刚来的路,有多少方案。其中j是r的第r个端点。r表示0或者1
f[i] [e] [r:0/1]<--f[i-1] [e'] [r':0/1],e'!=e,r'为e的第1-r个端点。
继续优化:f[i] [e1] [r1] [e2] [r2]:从e1的第r1个端点出发,走了i步,到达e2的第r2个端点,有多少合法路径(第1步走e1,第i步走e2)
for(e',r',e'',r'') f[i+i'] [e1] [r1] [e2] [r2] +=f[i] [e1] [r1] [e'] [r']*f[i'] [e''] [r''] [e2] [r2];
<(e',r')=(e'',r'')(前后两点相连),e'!=e''>
O(log(t*m^4));
再继续优化:
g[i] [e1] [r1] [v']=sum_{(e',r')=v'} f[i] [ei] [r1] [e'] [r']
g[i'] [v''] [e2] [r2]=sum_{(e''.r'')=v''} f[i'] [e''] [r''] [e2] [r2];
STEP1:f[i+i'] [e1] [r1] [e2] [r2] += f[i] [e1] [r1] [v]*f[i'] [v] [e2] [r2];
因为无法保证e’!=e,所以再用容斥原理减去相同的。
STEP2:f[i+i'] [e1] [r1] [e2] [r2] -= f[i] [e1] [r1] [e] [r] *f[i'] [e] [r] [e2] [r2];
代码:
#include<cstdio>
#include<cstring>
const int M = 65;
const int T = 31;
const int N = 25;
const int Mod = 45989;
int n,m,t,A,B;
int edge[M][2];
int f[T][M][2][M][2];//f[i]对应note里面2^i
int ans[M][2];//ans[i][j]表示从A出发,到达第i条边的第j个端点的方案数
int ansx[M][2];//计算时的辅助数组
int g[M][2][N],h[N][M][2];
void Add(int &x,int y)//x+=y (mod Mod)
{
x+=y;
if(x>=Mod)
x-=Mod;
}
void Del(int &x,int y)//x-=y (mod Mod)
{
x-=y;
if(x<0)
x+=Mod;
}
int main()
{
//freopen("1.txt","r",stdin);
int i,j,k,l,p,q,r;
scanf("%d%d%d%d%d",&n,&m,&t,&A,&B);
for(i=1;i<=m;i++)
scanf("%d%d",&edge[i][0],&edge[i][1]);
for(i=1;i<=m;i++)
{
f[0][i][0][i][1]=1;
f[0][i][1][i][0]=1;
}
for(i=1;i<T;i++)
{
memset(g,0,sizeof g);
memset(h,0,sizeof h);
for(j=1;j<=m;j++)
{
for(k=0;k<=1;k++)
{
for(l=1;l<=m;l++)
{
for(p=0;p<=1;p++)
{
Add(g[j][k][edge[l][p]],f[i-1][j][k][l][p]);
Add(h[edge[j][k]][l][p],f[i-1][j][k][l][p]);
}
}
}
}
for(j=1;j<=m;j++)
{
for(k=0;k<=1;k++)
{
for(l=1;l<=m;l++)
{
for(p=0;p<=1;p++)
{
for(q=0;q<n;q++)
{
Add(f[i][j][k][l][p],(g[j][k][q]*h[q][l][p])%Mod);
}
for(q=1;q<=m;q++)
{
for(r=0;r<=1;r++)
{
Del(f[i][j][k][l][p],(f[i-1][j][k][q][r]*f[i-1][q][r][l][p])%Mod);
}
}
}
}
}
}
}
ans[m+1][0]=1;
edge[m+1][0]=A;
for(i=0;i<T;i++)
{
if(t&(1<<i))
{
memset(ansx,0,sizeof ansx);
for(j=1;j<=m+1;j++)
{
for(k=0;k<=1;k++)//ans[j][k]
{
for(l=1;l<=m;l++)
{
for(p=0;p<=1;p++)
{
if(edge[j][k]==edge[l][p] && j!=l)
{
for(q=1;q<=m;q++)
{
for(r=0;r<=1;r++)
{
Add(ansx[q][r],(ans[j][k]*f[i][l][p][q][r])%Mod);
}
}
}
}
}
}
}
memset(ans,0,sizeof ans);
for(j=1;j<=m;j++)
{
for(k=0;k<=1;k++)
ans[j][k]=ansx[j][k];
}
}
}
int ansf=0;
for(j=1;j<=m;j++)
{
for(k=0;k<=1;k++)
{
if(edge[j][k]==B)
Add(ansf,ans[j][k]);
}
}
printf("%d
",ansf);
return 0;
}
例题:有n个木块排成一行,从左到右依次编号为1~n。 你有k种颜色的油漆,其中第i种颜色的油漆足够涂
ci个木块。所有油漆刚好足够涂满所有木块, 即
c1+c2+...+ck=n。相邻两个木块涂相同色显得很难 看,所以你希望统计任意两个相邻木块颜色不同的 着色方案。
模10^9+7
1 <= k <= 15, 1 <= ci <= 5
思路:
f[i1] [i2]....[ik] [p]
第f[i1] [i2]...[ik] [p]
第j种颜色用了ij次,染了前i1+i2+...+ik个木块。这些木块中,最后一个木块的颜色是p,有多少种方案。
for(p') if(p!=p')
f[i1] [i2]...[ip]...[ik] [p]+=f[i1] [i2]...[ip-1]...[ik] [p']
f[i0] [i1]...[i4] [p] 有i0种颜色染了0次,i1种颜色染了1次...i4种颜色染了4次(i5=k-i0-...-i4),最后一个木块染的颜色用了p次
for(p')
f[i0] [i1] [i2] [i3] [i4] [p]
+=f[i0]..[i{p-1}+1] [ip-1]..[i4] [p']*(i{p-1}) <p'=p-1>减去染色重色的
+=f[i0]..[i{p-1}+1] [ip-1]..[i4] [p'] *(i{p-1}+1) <p'!=p-1>
for(p)
ans+=f[0] [c1] [c2] [c3] [c4] [p]
优化空间
滚动数组
将数组的下标进行去模运算,滚动起来。
将二维数组转化为一维
加速转移
前缀和优化
单调性优化
改变转移方式
例题:n个小球,分成m组。
小球有编号两两不同,组没有编号不区分顺序。 求有多少种方案
m<=n<=1000
思路:dp[i] [j]=j*dp[i-1] [j]+dp[i-1] [j-1];
完全背包问题
优化1:二进制拆分;
把多重背包问题转化为0-1背包问题。
拆分方式:从1开始加前一项乘以2,如果总和超过原数,则把最后一个数删去,改为原数-前面所有以拆出数的总和
优化2:单调队列;
f[i] t[i]=3,v[i]=5,c[i]=4;
f[i] [1]=max(f[i-1] [1]);
f[i] [4]=5+max(f[i-1] [4]-5,f[i-1] [1]);
f[i] [7]=10+max(f[i-1] [7]-10,f[i-1] [4]-5,f[i-1] [1]);
f[i] [10]=15+max(f[i-1] [10]-15,f[i-1] [7]-10,f[i-1] [4]-5,f[i-1] [1]);
f[i] [13]=20+max(f[i-1] [13]-20,f[i-1] [10]-15,f[i-1] [7]-10,f[i-1] [4]-5,f[i-1] [1]);
f[i] [16]=25+max(f[i-1] [16]-25,f[i-1] [13]-20,f[i-1] [10]-15,f[i-1] [7]-10,f[i-1] [4]-5,f[i-1] [1]);
注意最后一行没有f[i-1] [1]了,因为到达了取物品个数的上线
求值时有两步:
STEP1:加和;
STEP2:后c个;
维护一个单调不上升序列。
最终只需要输出序列中的第一个,还需要判断第一个数有没有离现在的距离超过c。
若超过,继续向后找。