插入类dp
按结尾数字排名进行的插入类dp
T1 AT_dp_t Permutation
有一个长为 \(N\) 的正整数排列。给定一个由 <
和 >
组成长为 \(N-1\) 的的字符串。
对于任意满足 \(1 \le i \le N-1\) 的字符 \(s_i\),如果 \(s_i\) 是 <
则 \(P_i<P_{i+1}\)、如果 \(s_i\) 是 >
则 \(P_i>P_{i+1}\)。求满足这样的性质的排列 \(P\) 的方案数。
\(N\leq 3000\)
考虑动态规划
\(f[i][j]\)表示对于前i个数字,最后一个数字在前i个数字里排名第j的方案数。
比如样例4 <><
\(f[1][1]=1,f[2][1]=0,f[2][2]=1\)
1 2
\(f[3][1]=1,f[3][2]=1,f[3][3]=0\)
2 3 1,1 3 2
\(f[4][1]=0,f[4][2]=1,f[4][3]=2,f[4][4]=2\)
3 4 1 2,1 4 2 3,2 4 1 3,1 3 2 4,2 3 1 4
if(s[i-1]=='<')
\(f[i][j]=\sum_{k=1}^{j-1} f[i-1][k]\)
else
\(f[i][j]=\sum_{k=j}^{k=i-1}f[i-1][k]\)
前缀和优化之。
T2 AT_abc209_f Deforestation
\(n\) 个数:\(a_1,a_2,...,a_n\)。
每次可以选择一个 \(i\),选择的代价是 \(a_{i-1}+a_i+a_{i+1}\),然后令 \(a_i=0\)。
求有多少种方案,使得 \(a_1,a_2,...,a_n\) 都变为 \(0\) 的总代价最小。特别的,\(a_0=a_{n+1}=0\)
\(1\leq n\leq 4000,1\leq a_i\leq 10^9\)
考虑这个限制是什么。
对于\(i\)和\(i+1\)
先拿\(i\)再拿\(i+1\),收益是\(a[i]+2*a[i+1]\)
先拿\(i+1\)再拿\(i\),收益是\(a[i+1]+2*a[i]\)
显然,哪个大我希望先拿哪个。如果相等就随意。
于是和T1一样了
\(if(a[i-1]>a[i])\)
\(f[i][j]=\sum_{k=1}^{j-1} f[i-1][k]\)
\(else\ if(a[i-1]<a[i])\)
\(f[i][j]=\sum_{k=j}^{k=i-1}f[i-1][k]\)
\(else\)
\(f[i][j]=\sum_{k=1}^{i-1}f[i-1][k]\)
按段数进行的插入类dp
T3 P5999 kangaroo
有一个园子,里面有 \(n\) 个草丛排成一排,标号 \(1\sim n\),有一个袋鼠,从 \(s\) 出发,每次跳一步跳到一个其他的草丛,经过每个草丛恰好一次,最终到达 \(t\)。显然他会跳跃 \(n-1\)次为了不被人类发现,袋鼠每次跳跃的方向必须与前一次不同。
具体地,如果他现在在 \(now\),他是从 $prev $ 跳跃一次到达 \(now\) 的,然后他跳跃一次到达 \(next\):
-
那么如果 \(prev<now\),就必须有 \(next<now\);
-
如果 \(now<prev\),就必须有 \(now<next\)。
问从 \(s\) 到 \(t\) 的方案数模 \(10^9+7\)的结果。
两个路线不同,当且仅当草丛被访问的顺序不同。
保证至少有一种方案初始时可以往任意方向跳。
\(2\le n\le 2\times 10^3\),\(1\le s,t\le n\)
用\(f[i][j]\)表示前\(i\)个数字,分成了\(j\)段。我们规定每段都是一个元素或者低高低或者低高低高低或者低高低高低高低...
那么处理\(f[i][j]\)时,\(i\)对于前\(i-1\)个数字都是高的,所以他可以用来合并两个段,或者新开一段
\(f[i][j]=f[i-1][j+1]*j+f[i-1][j-1]*j\)
要求的序列可能是高低高低啊?我们接着往下看
s和t的关系如何解决?
考虑当\(i==s\)时,令\(f[i][j]=f[i-1][j-1]+f[i-1][j]\)。也就是新开一段放在最前,或者放在最前面的段的最前面。
考虑当\(i==t\)时,令\(f[i][j]=f[i-1][j-1]+f[i-1][j]\)。也就是新开一段放在最后,或者放在最前面的段的最后面。
放下来之后,我们修改普通的\(i\)的新开一个段的转移:
当\(i!=s\&\&i!=t\)时
\(f[i][j]=f[i-1][j+1]*j+f[i-1][j-1]*(j-(i>s)-(i>t))\)
也就是说,合并两个段的方案数不变,新开一段时,如果比st都大,就只能从中间新开(j-2)。如果大于s小于t,则可以在中间和最后新开(j-1)。如果小于s也小于t,就可以从中间和两边开(j)。
最后输出\(f[n][1]\)。
T4 CF704B Ant Man
- 有 \(n\) 个元素,第 \(i\) 个元素有五个参数 \(x_i,a_i,b_i,c_i,d_i\)。
- 你需要求出一个 \(1 \sim n\) 的排列 \(p\),满足 \(p_1 = s, p_n = e\),同时最小化这个排列的权值。
- 一个排列的权值为 \(\sum_{i=1}^{n-1} f(p_i, p_{i+1})\),其中 \(f(i,j)\) 的值有两种情况:
- 若 \(i > j\),则 \(f(i,j) = x_i - x_j + c_i + b_j\)。
- 若 \(i < j\),则 \(f(i,j) = x_j - x_i + d_i + a_j\)。
- \(n \le 5 \times 10^3\),\(s \ne e\),\(1 \le x_1 < x_2 < \cdots < x_n \le 10^9\),\(1 \le a_i,b_i,c_i,d_i \le 10^9\)。
用\(f[i][j]\)表示前i个数字,有j段的最小花费。
\(f[0][1]=0\)
普通的情况
\(f[i][j]=f[i-1][j-1]-x[i]*2+b[i]+d[i]\) 新开一段。
这一段必然是在未来作为高低高的低出现的,所以他对答案的贡献是-2x+b+d。
\(f[i][j]=f[i-1][j+1]+x[i]*2+a[i]+c[i]\)合并两个段。
这个点作为低高低的高出现,对答案的贡献是2x+c+a。
\(f[i][j]=f[i-1][j]+b[i]+c[i]\)往某个段前面放。
那他作为高中低的中出现,对答案的贡献是b+c。
\(f[i][j]=f[i-1][j]+a[i]+d[i]\)往某个段后面放。
那他作为低中高的中出现,对答案的贡献是a+d。
对于起点
\(f[i][j]=f[i-1][j]+x[i]+c[i]\)
连接在某个段前面,于是最终排列高低起手,s的贡献是x+c。
\(f[i][j]=f[i-1][j-1]-x[i]+d[i]\)
新开一个段,于是最终排列低高起手,s的贡献是-x+d。
对于终点
\(f[i][j]=f[i-1][j]+x[i]+a[i]\)
连接在某个段的后面,于是最终排列低高结尾,t的贡献是x+a。
\(f[i][j]=f[i-1][j-1]-x[i]+b[i]\)
新开一个段,于是最终排列高低结尾,t的贡献是-x+b。
考虑起点和终点固定下来对于普通的情况的转移有什么影响?
影响就是
想新开一段时,不能\(i>s\&\&i>t\&\&j-1==1\),此时st做起点终点,只有一段,就不能新开一段了。
想往某个段的前面放,要么s没出现,要么s出现了并且段数大于1才能往前放。
想往某个段的后面放,要么t没出现,要么t出现了并且段数大于1才能往后放。
输出\(f[n][1]\)
T5 P9197 摩天大楼
为了规避掉绝对值,我们可以排个序,就和上一题类似了
这题不规定左右端点了,考虑dp的时候加一维
\(f[i][j][x][sum]\)表示前i个数字,有j段,有x个端点,和为sum
if(j>=x)
f[i][j][x][sum]+=f[i-1][j-1][x][sum+2*a[i]]*(j-x);//新开一个,放在任意位置,贡献为-2ai
f[i][j][x][sum]+=f[i-1][j][x][sum]*(2*j-x);//放在某一段的左右,贡献为0
f[i][j][x][sum]+=f[i-1][j+1][x][sum-2*a[i]]*j;//合并两个,放在任意位置,贡献为2ai
if(x)
{
f[i][j][x][sum]+=f[i-1][j-1][x-1][sum+a[i]]*(3-x);//让ai新开一段
f[i][j][x][sum]+=f[i-1][j][x-1][sum-a[i]]*(3-x);//让ai做端点并且放在一段的左右
}
这样搞一搞,复杂度\(O(n^3L)\),不足以通过本题,\(L\)应该仍不掉了,考虑扔掉一个\(n\)。
插入\(a_i\)的时候,注意到\(a_i-a_{i-1}\)的贡献是和插入前的段数和端点数量相关的。
有\(j\)段,\(x\)个端点的时候,贡献为\((a[i]-a[i-1])*(2*j-x)\)。
由于我沿用\(ij\)寻找\(i-1\)的转移而来,所以代码写得很蠢。后来又写了一版从\(i\)向\(i+1\)转移的:
for(int i=0;i<n;i++)
for(int j=(i!=0);j<=i;j++)
for(int x=0;x<=2;x++)
for(int sum=0,tsum=(a[i+1]-a[i])*(2*j-x);tsum<=l;sum++,tsum++)
{
// int tsum=sum+(a[i+1]-a[i])*(2*j-x);
f[i+1][j+1][x][tsum]=(f[i+1][j+1][x][tsum]+f[i][j][x][sum]*(j+1-x))%mod;//新开一个
f[i+1][j][x][tsum]=(f[i+1][j][x][tsum]+f[i][j][x][sum]*(2*j-x))%mod;//放在某一段的左右
if(j)
f[i+1][j-1][x][tsum]=(f[i+1][j-1][x][tsum]+f[i][j][x][sum]*(j-1))%mod;//合并两个
if(x!=2)
{
f[i+1][j+1][x+1][tsum]=(f[i+1][j+1][x+1][tsum]+f[i][j][x][sum]*(2-x))%mod;//让ai新开一段
f[i+1][j][x+1][tsum]=(f[i+1][j][x+1][tsum]+f[i][j][x][sum]*(2-x))%mod;//让ai做端点
}
}
T6 P2612 波浪
一眼望去,和T5类似,只不过把\(a[i]=read()\)变为\(a[i]=i\)。
仔细一看,\(0\leq M\leq 2147483647\),貌似很唬人,但是仔细思考,可以发现循环到n*n/2即可,最大值不会比它更大。
int a[100],n,ans;
void dfs(int d){
if(d==n+1){
int sum=0;
for(int i=2;i<=n;i++)sum=sum+abs(a[i]-a[i-1]);
ans=max(ans,sum);
return ;
}
for(int i=d;i<=n;i++){
swap(a[i],a[d]);
dfs(d+1);
swap(a[i],a[d]);
}
}
int main(){
for(int i=1;i<=20;i++)a[i]=i;
for(n=1;n<=12;n++){
dfs(1);
cout<<n*n/2<<' '<<ans<<'\n';
}
}
由于要输出小数,不给模数,考虑使用__float128,并且使用函数输出。
void Print(__float128 ans){
int num[100];
num[0]=0;
ans=ans*10;
for(int i=1;i<K;i++){
num[i]=(int)ans;
ans=(ans-num[i])*10;
}
num[K]=(int)(ans+0.5);
for(int i=K;i>=1;i--)if(num[i]>=10)num[i]-=10,num[i-1]++;
printf("%d.",num[0]);
for(int i=1;i<=K;i++)printf("%d",num[i]);
puts("");
}
// 原文链接:https://blog.csdn.net/qq_35320178/article/details/89446547
然后需要滚动数组和卡常。我用了两个技巧,一个是\(if(!f[now][j][x][sum]) continue ;\),一个是并不边算边除以\(i\),而是最后再一个循环除一下\(n!\)。
T7 CF1515E Phoenix and Computers
我们继续,\(f[i][j]\)表示有\(i\)个电脑,组成了\(j\)个连续已开启的段。
这里的ij和T1类似,并非指最终开关顺序是1~n顺序地开,而是仅当一个占位符。
例如o是已开,\(xooxxoox\)这种方案是\(f[4][2]\)之一。
我们枚举i号电脑是怎么开开的:
如果i号电脑是紧挨某一段的:
\(f[i][j]+=f[i-1][j]*2*j\)
如果i号电脑是被动开开的,例如1 3 2
\(f[i][j]+=f[i-2][j]*2*j\)
如果最后一步是被动开的,并且合并了两个连续段
\(f[i][j]+=f[i-2][j+1]*2*j\)
\(f[i][j]+=f[i-3][j+1]*j\)
如果是新的连续段插入:
\(f[i][j]+=f[i-1][j-1]*j\)
T8 P7967 Magneti
显然,可以给r排个序,然后处理大r的时候只需要管大r的限制,不需要管小r的限制
大胆dp,优化是以后的事。
\(f[i][j][sum]\)表示用了前\(i\)个,\(j\)段,\(j\)段长度之和为\(sum\)的方案数。
if(sum>=r[i])f[i][j][sum]+=f[i-1][j][sum-r[i]]*j*2;
//放在某一段的边上
if(sum>=1)f[i][j][sum]+=f[i-1][j-1][sum-1]*j;
//新开一段
if(sum>=2*r[i])f[i][j][sum]+=f[i-1][j+1][sum-2*r[i]]*j;
//合并两段
最后还需要组合数一下。对于\(f[n][1][i]\),我们有\(l-i\)个空格给n+1个位置分配,挡板法,\(ans=\sum f[n][1][i]*C(n+l-i,l-i)\)。
T9 弹弹床 无数据
题意简述:给定序列 \({a_i}\in{0,1}\)。求长为\(n\) 的排列的数量,要求对于所有 \(i\in[1,n−1]\),如果 \(a_{p_i}=1\) 那么\(p_i>p_{i+1}\),否则\(p_i<p_{i+1}\),对每个\(k\) 求\(p_n=k\) 的方案数,\(n\leq 5000\)。