dp专题训练
Frog 1
我们设 \(f_i\) 表示跳到第 \(i\) 个石头的最小总费用。于是我们可以推出转移方程:
\(f_i=\min(f_{i-1}+|h_{i-1}-h_i|,f_{i-2}+|h_{i-2}-h_i|)\)。
当然这个方程在转移的时候不能越界。
于是做一个线性 \(dp\) 即可。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,h[N],f[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
}
memset(f,0x3f,sizeof f);
f[1]=0;
for(int i=1;i<=n;i++){
if(i>1){
f[i]=min(f[i],f[i-1]+abs(h[i]-h[i-1]));
}
if(i>2){
f[i]=min(f[i],f[i-2]+abs(h[i]-h[i-2]));
}
}
cout<<f[n];
return 0;
}
Frog 2
我们设 \(f_i\) 表示跳到第 \(i\) 个石头的最小总费用。于是我们可以推出转移方程:
\(\displaystyle f_i=\min_{j=\max(1,i-k)}^{j<i}(f_j+|h_i-h_j|)\)。
于是做一个线性 \(dp\) 即可,其实上一题就是 \(k=2\) 的特殊情况。
这里给个思考题:
- \(k=n\) 怎么做?
显然是好做的,答案就是 \(h_1-h_n\)。这个不难思考。因为如果中间跳到中转点,答案一定不会更优。所以不中转一定不劣。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,k,h[N],f[N];
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>h[i];
}
memset(f,0x3f,sizeof f);
f[1]=0;
for(int i=1;i<=n;i++){
for(int j=max(1ll,i-k);j<i;j++){
f[i]=min(f[i],f[j]+abs(h[i]-h[j]));
}
}
cout<<f[n];
return 0;
}
Vacation
首先我们按照原来的想法设 \(f_i\) 为第 \(i\) 天做完活动后的的最大幸福值。然后,就发现根本没法转移,因为题中的两天不能进行同一种活动的限制我们没有使用。我们考虑缺了这样一个东西,就把它加到状态里。
设 \(f{i,j}\) 表示第 \(i\) 天做的是第 \(j\) 个活动,能获得的最大幸福值,这里 \(j=0/1/2\)。
转移方程非常好推:\(\displaystyle f_{i,j}=\max_{j}^{j\ne i}(f_{i-1,j}+val_j)\),\(val_j\) 为第 \(j\) 种活动方式获得的幸福度。
提示 \(1\):设状态后发现这个状态出现了问题,可以把出现问题的这个东西加到状态里。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,a[N],b[N],c[N],f[N][3];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i]>>c[i];
}
f[1][0]=a[1];
f[1][1]=b[1];
f[1][2]=c[1];
for(int i=2;i<=n;i++){
f[i][0]=max(f[i-1][1],f[i-1][2])+a[i];
f[i][1]=max(f[i-1][0],f[i-1][2])+b[i];
f[i][2]=max(f[i-1][0],f[i-1][1])+c[i];
}
cout<<max({f[n][0],f[n][1],f[n][2]});
return 0;
}
Knapsack 1
我们考虑设 \(f_{i,j}\) 为考虑前 \(i\) 个物品,背包容量为 \(j\) 能获得的最大价值。那么这个方程非常好推:
\(\displaystyle f_{i,j}=\max_{j}^{j\ge w_i}(f_{i-1,j-w_i}+v_i)\)。
到这里已经可以通过了,但是我们还能做的更好。
可以发现 \(i\) 在转移时只会用到 \(i-1\)。于是我们第一维不需要开 \(n\) 的大小,只需要开 \(2\) 的大小即可。
提示 \(2\):在空间比较紧张且 \(i\) 的转移只需要用到 \(i-1\) 时,可以采用滚动数组优化,把其中一维的空间变为常数级别。
事实上可以继续优化。我们可以把第一维直接优化掉,我们在循环 \(j\) 时进行倒序循环,这样就可以在线性空间内解决 \(01\) 背包问题。
补充一句,完全背包也可以这样做,但是应用完全背包时 \(j\) 需要正序循环。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 105
#define M 100005
using namespace std;
int n,m,w[N],v[N],f[M];
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
}
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
cout<<f[m];
return 0;
}
Knapsack 2
我们考虑设 \(f_{i,j}\) 为考虑前 \(i\) 个物品,背包容量为 \(j\) 能获得的最大价值。然后就发现,空间上不能接受,即使优化掉第一维。
但是,可以发现 \(v_i\) 非常的小,那么,能不能转换一下状态,设 \(f_{i,j}\) 为前 \(i\) 个物品,获得的价值为 \(j\) 所需的最小背包容量。于是有两个转移方程:
-
\(f_{i,j}=\min(f_{i,j},f_{i-1,j})\)。
-
\(\displaystyle f_{i,j}=\min_{j}^{j\ge v_i}(f_{i,j},f{i-1,j-v_i}+w_i)\)。
答案非常好求,就是找到最大的 \(i\),使得 \(f_{n,i}\le m\),答案就是这个 \(i\)。
提示 \(3\):如果一个状态会炸空间并且无法优化且此时答案的值域很小,可以将答案作为状态,状态作为答案。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 105
#define M 100005
using namespace std;
int n,m,w[N],v[N],f[N][M];
signed main(){
cin>>n>>m;
int sum=0;
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
sum+=v[i];
}
memset(f,0x3f,sizeof f);
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=sum;j>=0;j--){
f[i][j]=min(f[i][j],f[i-1][j]);
if(j>=v[i])f[i][j]=min(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
int res=0;
for(int i=sum;i>=0;i--){
if(f[n][i]<=m){
res=i;
break;
}
}
cout<<res;
return 0;
}
LCS
最长公共子序列的状态十分经典。设 \(f_{i,j}\) 为 \(s\) 的前 \(i\) 个字符和 \(t\) 的前 \(j\) 个字符的最长公共子序列的长度。转移方程:
\(f_{i,j}=\max(f_{i-1,j},f_{i,j-1})\)。
当 \(s_i\) 与 \(t_j\) 相同时,\(f_{i,j}=\max(f_{i,j},f_{i-1,j-1}+1)\)。
这样我们就可以求出整个序列的最长公共子序列的长度了。我们可以从这个倒推出方案。
分三种情况讨论:
-
\(s_i\) 与 \(t_j\) 相同且 \(f_{i,j}=f_{i-1,j-1}+1\):那么答案的第 \(f_{i,j}\) 位就是 \(s_i\)。然后 \(i=i-1,j=j-1\)。
-
\(f_{i,j}\) 与 \(f_{i-1,j}\) 相同:\(i=i-1\)。
-
\(f_{i,j}\) 与 \(f_{i,j-1}\) 相同:\(j=j-1\)。
这里事实上就是枚举 \(f_{i,j}\) 是由 \(3\) 种转移方式的哪一个转移而来的,倒推回去,其实拿个数组记录也可以,就是有点麻烦。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 3005
using namespace std;
int n,m,f[N][N];
char s[N],t[N],res[N];
signed main(){
cin>>s+1>>t+1;
n=strlen(s+1);
m=strlen(t+1);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j]=max({f[i-1][j],f[i][j-1]});
if(s[i]==t[j])f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
}
int i=n,j=m;
while(f[i][j]!=0){
if(s[i]==t[j]&&f[i][j]==f[i-1][j-1]+1){
res[f[i][j]]=s[i];
i--;j--;
}
else if(f[i][j]==f[i-1][j])i--;
else if(f[i][j]==f[i][j-1])j--;
}
for(int i=1;i<=f[n][m];i++){
cout<<res[i];
}
return 0;
}
Longest Path
我们设 \(f_i\) 为以节点 \(i\) 为终点的最长路长度,于是对于每个 \(i\) 的前驱节点 \(t\),有转移方程:
\(f_i=\max(f_i,f_j+1)\)。
写一个拓扑排序进行转移即可。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,h[N],e[N],ne[N],idx,din[N],f[N];
void add(int a,int b){
e[idx]=b;ne[idx]=h[a];h[a]=idx++;din[b]++;
}
signed main(){
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
add(a,b);
}
queue<int>q;
for(int i=1;i<=n;i++){
if(din[i]==0){
q.push(i);
}
}
while(!q.empty()){
int t=q.front();
q.pop();
for(int i=h[t];~i;i=ne[i]){
int j=e[i];
f[j]=max(f[j],f[t]+1);
if(--din[j]==0)q.push(j);
}
}
int res=0;
for(int i=1;i<=n;i++){
res=max(res,f[i]);
}
cout<<res;
return 0;
}
Grid 1
我们设 \(f_{i,j}\) 表示走到 \((i,j)\) 格子的方案数。若 \(a_{1,1}\) 不为 #
,则 \(f_{1,1}=1\)。于是在 \(a_{i,j}\) 不为 #
,有转移方程:
\(f_{i,j}=f_{i-1,j}+f_{i,j-1}\)。
这里给个思考题:
- 没有障碍且 \(h,w\le 10^5\) 怎么做。
答案是 \(C_{h+w-2,h-1}\)。考虑为什么是这样?事实上一共要走 \(h+w-2\) 步,然后选择 \(i-1\) 步向下,其余的向右,所以是这样一个组合数问题。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 1005
#define mod 1000000007
using namespace std;
int n,m,f[N][N];
char a[N][N];
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
if(a[1][1]!='#')f[1][1]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i][j]=='#')continue;
if(i==1&&j==1)continue;
f[i][j]=(f[i-1][j]+f[i][j-1])%mod;
}
}
cout<<f[n][m];
return 0;
}
Coins
我们设 \(f_{i,j}\) 为前 \(i\) 枚硬币,\(j\) 枚正面朝上的概率。初始化 \(f_{0,0}=1\)。转移方程有两种:
\(f_{i,j}=f_{i,j}+(1-p_i)\times f_{i-1,j}\)。
\(j>0,f_{i,j}=f_{i,j}+p_i\times f_{i-1,j-1}\)。
于是最终答案为 \(\displaystyle\sum_{i=\lfloor\frac{n}{2}\rfloor+1}^{i\le n}f_{n,i}\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 3005
using namespace std;
int n;
double p[N],f[N][N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>p[i];
}
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=i;j++){
f[i][j]+=(1-p[i])*f[i-1][j];
if(j!=0)f[i][j]+=p[i]*f[i-1][j-1];
}
}
double res=0;
for(int i=n/2+1;i<=n;i++){
res+=f[n][i];
}
cout<<setiosflags(ios::fixed)<<setprecision(10);
cout<<res;
return 0;
}
Sushi
首先设 \(f_{i,j,k,l}\) 为当前剩余 \(i\) 个盘子中有 \(0\) 个寿司,\(j\) 个盘子中有 \(1\) 个寿司,\(k\) 个盘子中有 \(2\) 个寿司,\(l\) 个盘子中有 \(3\) 个寿司的期望吃完次数。
首先会发现空间炸了,但是我们先不管空间,先去推一下转移方程:
考虑分讨吃到的盘子中的寿司数量,于是有:
\(f_{i,j,k,l}=f_{i,j,k,l}\times p_1+f_{i+1,j-1,k,l}\times p_2+f_{i,j+1,k-1,l}\times p_3+f_{i,j,k+1,l-1}\times p_4+1\)。其中的变量 \(p_1=\frac{i}{n},p_2=\frac{j}{n},p_3=\frac{k}{n},p_4=\frac{l}{n}\)。
移项得到:\(f_{i,j,k,l}=f_{i+1,j-1,k,l}\times\frac{j}{n-i}+f_{i,j+1,k-1,l}\times\frac{k}{n-i}+f_{i,j,k+1,l-1}\times\frac{l}{n-i}+\frac{n}{n-i}\)。
然后我们有 \(i+j+k+l=n\),所以可以用 \(j+k+l\) 替换 \(n-i\)。
发现现在的方程除了空间太大没有其他问题,又因为 \(i=j+k+l\),于是我们舍弃第一维 \(i\),把状态设为 \(f_{i,j,k}\)(注意这里 \(i\) 代表的是原来的 \(j\),\(j,k\) 以此类推),表示为当前剩余 \(i\) 个盘子中有 \(1\) 个寿司,\(j\) 个盘子中有 \(2\) 个寿司,\(k\) 个盘子中有 \(3\) 个寿司的期望吃完次数。
最终的方程:\(f_{i,j,k}=f_{i-1,j,k}\times\frac{i}{i+j+k}+f_{i+1,j-1,k}\times\frac{j}{i+j+k}+f_{i,j+1,k-1}\times\frac{k}{i+j+k}+\frac{n}{i+j+k}\),注意不要越界。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 305
using namespace std;
int n,a[N];
double f[N][N][N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
int x;
cin>>x;
a[x]++;
}
for(int k=0;k<=n;k++){
for(int j=0;j<=n;j++){
for(int i=0;i<=n;i++){
if(i==0&&j==0&&k==0)continue;
double &v=f[i][j][k];
if(i!=0)v+=f[i-1][j][k]*i/(i+j+k);
if(j!=0)v+=f[i+1][j-1][k]*j/(i+j+k);
if(k!=0)v+=f[i][j+1][k-1]*k/(i+j+k);
v+=1.0*n/(i+j+k);
}
}
}
cout<<setiosflags(ios::fixed)<<setprecision(14);
cout<<f[a[1]][a[2]][a[3]];
return 0;
}
Stones
我们设 \(f_i\) 用来表示还剩 \(i\) 个石子时此人必胜还是必败(为 \(1\) 还是 \(0\))。
可以发现,\(f_i\) 能够转移到如果一个必败的状态,那么剩下 \(i\) 个石子时一定必胜,否则必败。
所以转移方程为:当存在一个 \(f_{i-a_j}=0\),\(f_i=1\),否则 \(f_i=0\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 105
#define K 100005
using namespace std;
int n,k,a[N],f[K];
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=k;i++){
for(int j=1;j<=n;j++){
if(i<a[j])continue;
if(f[i-a[j]]==0)f[i]=1;
}
}
if(f[k]==1)cout<<"First";
else cout<<"Second";
return 0;
}
Deque
我们设 \(f_{i,j}\) 为还剩下区间 \([i,j]\) 的数时,先手取到的数减去后手取到的数的最大值。可以发现这是一个区间 \(dp\)。于是我们分类讨论接下来该谁取数,这可以用已经取的数的奇偶性判断。
如果是该先手取数:\(f_{i,j}=\max(f_{i+1,j}+a_i,f_{i,j-1}+a_j)\)。
如果是该后手取数:\(f_{i,j}=\min(f_{i+1,j}-a_i,f_{i,j-1}-a_j)\)。
提示4:区间 \(dp\) 应该先枚举区间长度,再枚举左端点,然后根据这两个东西确定右端点。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 3005
using namespace std;
int n,a[N],f[N][N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int len=1;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
int c=n-len;
if(len==1){
if(c%2==0)f[i][j]=a[i];
else f[i][j]=-a[i];
continue;
}
if(c%2==0)f[i][j]=max(f[i+1][j]+a[i],f[i][j-1]+a[j]);
else f[i][j]=min(f[i+1][j]-a[i],f[i][j-1]-a[j]);
}
}
cout<<f[1][n];
return 0;
}
Candies
这是我们的第一道 \(dp\) 优化,会进行比较详细的说明。
我们设 \(f_{i,j}\) 为前 \(i\) 个人分走 \(j\) 颗糖的方案数。初始化 \(f_{0,0}=1\),于是转移是好推的:
\(\displaystyle f_{i,j}=\sum_{k=\max(0,j-a_i)}^{j}f_{i-1,k}\)。
然后就发现,这个东西的复杂度过高,所以要采取一些优化。
发现一个事情,我们慢的原因是每次都要求一次和,这个东西就是我们的瓶颈。
考虑对这个和式做前缀和,然后在转移的时候直接使用记录前缀和转移,这样就可以通过了。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 105
#define K 100005
#define mod 1000000007
using namespace std;
int n,k,a[N],f[N][K],sum[K];
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
f[0][0]=1;
for(int i=1;i<=n;i++){
sum[0]=f[i-1][0];
for(int j=1;j<=k;j++){
sum[j]=(sum[j-1]+f[i-1][j])%mod;
}
for(int j=0;j<=k;j++){
int pos=max(0ll,j-a[i]);
if(pos==0){
(f[i][j]+=sum[j])%=mod;
}
else{
(f[i][j]+=sum[j]-sum[pos-1])%=mod;
}
f[i][j]=(f[i][j]+mod)%mod;
}
}
cout<<f[n][k];
return 0;
}
Slimes
我们设 \(f_{i,j}\) 为合并区间 \([i,j]\) 的所有数的最小代价。于是我们对于所有的 \(k\in[i,j-1]\),有转移:
\(f_{i,j}=\min(f_{i,j},f_{i,k}+f_{k+1,j}+sum_j-sum_{i-1})\)。
这里的 \(sum_i\) 为前缀和,代表 \(\displaystyle\sum_{j=1}^{i}a_j\)。
这里为什么要加上 \([i,j]\) 区间所有数的和呢?是因为合并时的代价是两个数的和,而这两个数初始是这个区间的所有数,所以这两个数的和一定是区间 \([i,j]\) 内的数的和。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 405
using namespace std;
int n,a[N],sum[N],f[N][N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
memset(f,0x3f,sizeof f);
for(int len=1;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(len==1){
f[i][j]=0;
continue;
}
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
}
}
}
cout<<f[1][n];
return 0;
}
Matching
我们设 \(f_i\) 为前 \(j\) 个男人与 \(i\) 集合中的女人的完备匹配方案数。其中 \(i\) 是一个状态压缩表示的集合,\(j\) 是集合 \(i\) 的大小。
所谓状态压缩,一般是用二进制表示某个东西的有无(当然也可能用其他进制表示一些复杂的东西)。这里 \(i\) 在二进制下的某一位如果是 \(1\),那么代表这个人在集合中。
于是有转移方程:\(f_i=f_i+f_{i-2^j}\),要求 \(j\) 在 \(i\) 集合内,且 \(j\) 与 \(k\) 之间有边(这里 \(k\) 是集合 \(i\) 的大小)。注意下面的代码因为下标从 \(0\) 开始,所以把 \(k\) 减去了 \(1\)。
提示 \(5\):在表示一个集合且集合大小比较小时,可以用二进制状态压缩法表示。
提示 \(6\):
__builtin_popcount(i)
是指 \(i\) 二进制表示下有多少位是 \(1\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 21
#define M 1<<N
#define mod 1000000007
#define pct __builtin_popcount
using namespace std;
int n,a[N][N],f[M];
signed main(){
cin>>n;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
cin>>a[i][j];
}
}
f[0]=1;
for(int i=1;i<1<<n;i++){
int k=pct(i)-1;
for(int j=0;j<n;j++){
if((i>>j&1)&&(a[j][k]==1)){
(f[i]+=f[i-(1<<j)])%=mod;
}
}
}
cout<<f[(1<<n)-1];
return 0;
}
Independent Set
我们设 \(f_{i,0/1}\) 表示 \(i\) 节点染黑/白其子树的方案数。首先把所有的 \(f_{i,0/1}\) 初始化为 \(1\),然后对于所有 \(i\) 的儿子 \(j\),就有转移方程:
\(f_{i,0}=f_{i,0}\times f_{j,1}\)。
\(f_{i,1}=f_{i,1}\times (f_{j,0}+f_{j,1})\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M N<<1
#define mod 1000000007
using namespace std;
int n,h[N],e[M],ne[M],idx,f[N][2];
void add(int a,int b){
e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void dfs(int u,int fa){
f[u][0]=f[u][1]=1;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==fa)continue;
dfs(j,u);
(f[u][0]*=f[j][1])%=mod;
(f[u][1]*=(f[j][0]+f[j][1]))%=mod;
}
}
signed main(){
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
add(a,b);add(b,a);
}
dfs(1,0);
cout<<(f[1][0]+f[1][1])%mod;
return 0;
}
Flowers
首先引入一个东西,对于一个长度为 \(n\) 且 \(n\le 5\times 10^3\) 的序列,求它的最长上升子序列的长度,只需设 \(f_i\) 为以 \(i\) 结尾的最长上升子序列的长度。
于是在 \(h_j<h_i\) 时,有转移方程:\(f_i=\max(f_i,f_j+1)\)。
发现这样做有点慢,瓶颈在于遍历所有的 \(j\)。所以我们考虑能否快速查找。
事实上,我们可以把 \(f_i\) 插入进树状数组内以快速查询最大值。
现在回到这道题,我们设 \(f_i\) 表示以 \(i\) 结尾的最长上升子序列的花的权值和。
于是在 \(h_j<h_i\) 时,有转移方程:\(f_i=\max(f_i,f_j+a_j)\)。
只需把 \(f_i\) 插入进树状数组内即可快速查询当 \(h_j<h_i\) 时 \(f_j\) 的最大值。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,a[N],b[N],c[N],f[N];
int lowbit(int x){
return x&-x;
}
void modify(int x,int v){
while(x<=n){
c[x]=max(c[x],v);
x+=lowbit(x);
}
}
int qry(int x){
int res=0;
while(x){
res=max(res,c[x]);
x-=lowbit(x);
}
return res;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
f[i]=qry(a[i]-1)+b[i];
modify(a[i],f[i]);
}
int res=0;
for(int i=1;i<=n;i++){
res=max(res,f[i]);
}
cout<<res;
return 0;
}
Walk
我们设 \(f_{t,i,j}\) 表示 \(i\rightarrow j\) 的长为 \(t\) 的路径。
于是有转移方程:\(\displaystyle f_{t,i,j}=\sum_{k=1}^n(f_{t-1,i,k}\times f_{1,k,j})\)。
因为如果 \(i\rightarrow k\) 长为 \(t-1\),\(k\rightarrow j\) 长为 \(1\),故会产生一条 \(i\rightarrow j\) 长度为 \(t\) 的路径。
但是我们发现如果这样做 \(k\) 次,时间上不可接受。
观察一个东西,我们可以发现 \(f_t=f_{t-1}\times f_1\) 这等价于 \(f_t=f_1^t\)。于是我们使用矩阵快速幂进行优化。就是把 \(f_1\) 近似看成一个数,用快速幂和矩阵乘法的方法计算其 \(k\) 次方所对应的矩阵。
提示 \(7\):在矩阵乘法中代表单位 \(1\) 的矩阵具有 \(f_{i,i}=1\) 且其他值全为 \(0\) 的特点。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 55
#define mod 1000000007
using namespace std;
int n,k;
struct node{
int f[N][N];
}a;
node operator*(node a,node b){
node res;
memset(res.f,0,sizeof res.f);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
(res.f[i][j]+=a.f[i][k]*b.f[k][j]%mod)%=mod;
}
}
}
return res;
}
node ksm(node x,int y){
node res;
memset(res.f,0,sizeof res.f);
for(int i=1;i<=n;i++){
res.f[i][i]=1;
}
while(y){
if(y&1)res=res*x;
x=x*x;
y>>=1;
}
return res;
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cin>>a.f[i][j];
}
}
node b=ksm(a,k);
int res=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
(res+=b.f[i][j])%=mod;
}
}
cout<<res;
return 0;
}
Digit Sum
我们考虑设 \(f_{i,j,1/0}\) 表示前 \(i\) 位的和对 \(d\) 取余的结果为 \(j\),并且当前的前 \(i\) 位是/否全部与上界相同。
于是我们可以使用记忆化搜索转移。转移方程:\(f_{i,j,k}=f_{i,j,k}+f_{i-1,(j+digit)\bmod d,k \operatorname{and} [digit=limit]}\)。
这里 \(digit\) 为这一位的数,\(limit\) 为当前能选到的最大值。
转移边界为 \(i=0\) 时,如果 \(j\) 正好是 \(0\),则 \(f_{i,j,k}\) 为 \(1\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 10005
#define D 105
#define mod 1000000007
using namespace std;
int d,num[N],cnt,f[N][D][2];
string k;
int dfs(int dep,int sum,bool is_lim){
if(dep==0)return sum==0;
int &v=f[dep][sum][is_lim];
if(v!=-1)return v;
int lim=9;
if(is_lim)lim=num[dep];
int res=0;
for(int i=0;i<=lim;i++){
(res+=dfs(dep-1,(sum+i)%d,is_lim&&(i==lim)))%=mod;
}
return v=res;
}
int solve(){
for(int i=k.size()-1;i>=0;i--){
num[++cnt]=k[i]-'0';
}
return dfs(cnt,0,1);
}
signed main(){
memset(f,-1,sizeof f);
cin>>k>>d;
int res=solve()-1;
res=(res+mod)%mod;
cout<<res;
return 0;
}
Permutation
我们设 \(f_{i,j}\) 表示前 \(i\) 位放 \(1\sim i\),且第 \(i\) 位在前 \(i\) 个数中是第 \(j\) 小的方案数。
于是我们分类讨论得出转移方程:
当字符为 <
,\(\displaystyle f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k}\)。
当字符为 >
,\(\displaystyle f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k}\)。
不难发现又是使用一个和式转移,所以我们对 \(f_{i-1}\) 记录前缀和,用前缀和优化转移即可。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 3005
#define mod 1000000007
using namespace std;
int n,f[N][N],sum[N];
char s[N];
signed main(){
cin>>n;
for(int i=2;i<=n;i++){
cin>>s[i];
}
f[1][1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
sum[j]=(sum[j-1]+f[i-1][j])%mod;
}
for(int j=1;j<=i;j++){
if(s[i]=='<'){
(f[i][j]+=sum[j-1])%=mod;
}
else{
(f[i][j]+=sum[i-1]-sum[j-1])%=mod;
}
f[i][j]=(f[i][j]+mod)%mod;
}
}
int res=0;
for(int i=1;i<=n;i++){
(res+=f[n][i])%=mod;
}
cout<<res;
return 0;
}
Grouping
我们设 \(f_i\) 表示已选的集合为 \(i\) 的最大权值,于是对于每个 \(i\) 的子集 \(j\),有转移方程(注意 \(i,j\) 都是二进制数状态压缩表示的集合):
\(f_i=\max(f_i,f_j+val_{i-j})\)。
这里 \(i-j\) 表示如果把 \(i-j\) 对应的集合全放到一组的贡献,事实上他可以预处理。
然后就是,枚举子集可以用位运算表示,复杂度 \(3^n\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 16
#define M 1<<N
using namespace std;
int n,f[M],val[M],a[N][N];
signed main(){
cin>>n;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
cin>>a[i][j];
}
}
for(int i=0;i<1<<n;i++){
for(int j=0;j<n;j++){
if(!(i>>j&1))continue;
for(int k=j+1;k<n;k++){
if(!(i>>k&1))continue;
val[i]+=a[j][k];
}
}
f[i]=val[i];
}
for(int i=0;i<1<<n;i++){
for(int j=i;j;j=j-1&i){
f[i]=max(f[i],f[j]+val[i-j]);
}
}
cout<<f[(1<<n)-1];
return 0;
}
Subtree
我们设 \(f_u\) 为把 \(u\) 涂黑且连通块在 \(u\) 子树内的方案数,\(g_u\) 为把 \(u\) 涂黑且连通块不在 \(u\) 子树内的方案数。
于是对于 \(u\) 的儿子 \(j\) 有转移方程:\(\displaystyle f_u=\prod(f_j+1)\)。
设 \(fa\) 为 \(u\) 的父亲,那么对于 \(u\) 的兄弟 \(v\) 有转移方程:\(\displaystyle g_u=g_{fa}\times\prod(f_j+1)+1\)。
\(g\) 这样转移的原因是:首先不在 \(u\) 子树内则要么在祖先处,要么在它兄弟子树内。而祖先处已经计算出了 \(g{fa}\),直接继承即可。后面的 \((f_j+1)\) 就是其兄弟子树内的方案,多出来的 \(1\) 是全染白色。
如果我们暴力找 \(u\) 的兄弟是会超时的,所以考虑怎么优化。
我们可以对于每个 \(u\),都预处理他前面的所有兄弟的 \((f_j+1)\) 之积和他后面的所有兄弟的 \((f_j+1)\) 之积。这样我们对于每个点,就可以用前后缀积的乘积相乘得到原先我们需要暴力计算的式子。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,mod,f[N],g[N],t1[N],t2[N];
vector<int>e[N];
void add(int a,int b){
e[a].push_back(b);
}
void dfs1(int u,int fa){
f[u]=1;
vector<int>son;
for(int j:e[u]){
if(j==fa)continue;
dfs1(j,u);
(f[u]*=(f[j]+1))%=mod;
son.push_back(j);
}
int p1=1,p2=1;
for(int j:son){
t1[j]=p1;
(p1*=(f[j]+1))%=mod;
}
reverse(son.begin(),son.end());
for(int j:son){
t2[j]=p2;
(p2*=(f[j]+1))%=mod;
}
}
void dfs2(int u,int fa){
if(fa==0)g[u]=1;
else g[u]=(g[fa]*t1[u]%mod*t2[u]%mod+1)%mod;
for(int j:e[u]){
if(j==fa)continue;
dfs2(j,u);
}
}
signed main(){
cin>>n>>mod;
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
add(a,b);add(b,a);
}
dfs1(1,0);
dfs2(1,0);
for(int i=1;i<=n;i++){
cout<<f[i]*g[i]%mod<<'\n';
}
return 0;
}
Intervals
我们先考虑一个朴素的 \(dp\),设 \(f_{i,j}\) 为考虑前 \(i\) 个位置,最后一个 1
放在 \(j\) 的最大分数。
于是有转移方程:\(\forall j<i,f_{i,j}=f_{i-1,j}+\sum_{l_k\le j,r_k=i}v_k\),其中 \(v_k\) 为第 \(k\) 个需求被满足获得的分数。注意这里 \(f_{i,i}\) 应该直接用所有 \(j\le i-1\) 的 \(f_{i-1,j}\) 最大值更新 \(f_{i,i}\),再加上新产生的分数。
空间是好优化的,直接把 \(i\) 这一维去掉或者使用滚动数组优化都可以。
接下来考虑时间上的优化。可以发现每一次都是一个区间的 \(f\) 同时增加了一个值。不难发现这很像线段树的区间加操作。
于是,我们先把所有区间按照 \(r\) 排序,然后我们采用线段树维护 \(f\) 数组,每次区间加上一个分数,最后答案即为全局最大值。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,f[N<<2],lzy[N<<2];
struct node{
int l,r,v;
bool operator<(const node &t)const{
return r<t.r;
}
}a[N];
void pushup(int u){
f[u]=max(f[u<<1],f[u<<1|1]);
}
void pushdown(int u){
f[u<<1]+=lzy[u];
f[u<<1|1]+=lzy[u];
lzy[u<<1]+=lzy[u];
lzy[u<<1|1]+=lzy[u];
lzy[u]=0;
}
void modify(int u,int l,int r,int L,int R,int v){
if(l>=L&&r<=R){
f[u]+=v;
lzy[u]+=v;
return;
}
int mid=l+r>>1;
pushdown(u);
if(L<=mid)modify(u<<1,l,mid,L,R,v);
if(R>mid)modify(u<<1|1,mid+1,r,L,R,v);
pushup(u);
}
int qry(){
return max(f[1],0ll);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a[i].l>>a[i].r>>a[i].v;
}
sort(a+1,a+m+1);
int j=1;
for(int i=1;i<=n;i++){
modify(1,1,n,i,i,qry());
while(j<=m&&a[j].r==i){
modify(1,1,n,a[j].l,a[j].r,a[j].v);
j++;
}
}
cout<<qry();
return 0;
}
Tower
考虑 \(i,j\) 谁放到上面会更优。如果 \(i\) 在 \(j\) 上面,则剩下 \(s_j-w_i\),否则剩下 \(s_i-w_j\)。考虑移项,得出 \(s_i+w_i>s_j+w_j\) 时 \(i\) 在下面更优,否则 \(j\) 在下面更优。
于是我们按照这个排个序,然后写一个背包就做完了。
具体地,设 \(f_j\) 表示重量为 \(j\) 时的最大价值,有 \(f_j=\max(f_j,f_{j-w_i}+v_i)\),注意不要越界。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 1005
#define M 20005
using namespace std;
int n,f[M];
struct node{
int w,s,v;
bool operator<(const node &t)const{
return w+s<t.w+t.s;
}
}a[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i].w>>a[i].s>>a[i].v;
}
sort(a+1,a+n+1);
int res=0;
for(int i=1;i<=n;i++){
for(int j=a[i].w+a[i].s;j>=a[i].w;j--){
f[j]=max(f[j],f[j-a[i].w]+a[i].v);
res=max(res,f[j]);
}
}
cout<<res;
return 0;
}
Grid 2
首先之前提到过一个结论,对于没有障碍的网格,从 \((1,1)\) 走到 \((n,m)\) 的方案数为 \(C_{n+m-2}^{n-1}\),这个不难理解。
于是我们设 \(f_i\) 为走到第 \(i\) 个障碍的合法方案数,于是 \(f_i\) 的初始化就为上面的那个式子。
接下来,我们找到所有走到第 \(i\) 个障碍所必须经过的障碍 \(j\),事实上这里就是对坐标排个序然后找出 \(x_j\le x_i\) 且 \(y_j\le y_i\) 的所有 \(j\)。然后就有转移方程:
\(f_i=f_i-f_j\times C_{x_i-x_j+y_i-y_j}^{x_i-x_j}\)。
最后就是把终点当作第 \(n+1\) 个障碍,答案即为 \(f_{n+1}\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 3005
#define M 200005
#define mod 1000000007
using namespace std;
int h,w,n,fac[M],inv[M],f[N];
struct node{
int x,y;
bool operator<(const node &t)const{
if(x==t.x)return y<t.y;
return x<t.x;
}
}a[N];
int ksm(int x,int y){
int res=1;
while(y){
if(y&1)(res*=x)%=mod;
(x*=x)%=mod;
y>>=1;
}
return res;
}
void init(){
fac[0]=1;
inv[0]=1;
for(int i=1;i<M;i++){
fac[i]=fac[i-1]*i%mod;
inv[i]=ksm(fac[i],mod-2);
}
}
int c(int n,int m){
return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main(){
init();
cin>>h>>w>>n;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y;
}
a[++n]={h,w};
sort(a+1,a+n+1);
for(int i=1;i<=n;i++){
f[i]=c(a[i].x+a[i].y-2,a[i].x-1);
for(int j=1;j<i;j++){
if(a[j].y>a[i].y)continue;
f[i]-=f[j]*c(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod;
f[i]=(f[i]+mod)%mod;
}
}
cout<<f[n];
return 0;
}
Frog 3
我们设 \(f_i\) 表示跳到第 \(i\) 个石头的最小花费。可以发现朴素转移是会超时的,于是考虑优化。
首先给出方程:\(f_i=\min(f_i,f_j+(h_i-h_j)^2+c)\),考虑能否快速求出后面这个东西的最小值。
我们把它列出来:\(f_j+(h_i-h_j)^2+c\),然后展开:\(f_j+h_i^2+h_j^2-2\times h_i\times h_j+c\)。可以发现只含 \(i\) 的项和 \(c\) 可以看作常量。
我们假设 \(j_1\) 比 \(j_2\) 转移更优,于是有:\(f_{j_1}+h_{j_1}^2-2\times h_i\times h_{j_1}\le f_{j_2}+h_{j_2}^2-2\times h_i\times h_{j_2}\)。我们再设 \(g_j\) 为 \(f_j+h_j^2\),以及 \(k\) 为 \(-2\times h_i\)。可推出:\(g_{j_1}-2\times k\times h_{j_1}\le g_{j_2}-2\times k\times h_{j_2}\)。
注意 \(h\) 是单调递增的,然后把上面的式子做一个移项得到:\(-2\times k\times(h_{j_1}-h_{j_2})\le g_{j_2}-g_{j_1}\),除过去,得到:\(-2\times k\ge\frac{g_{j_2}-g_{j_1}}{h_{j_1}-h_{j_2}}\),两边同时变号得:\(2\times k\le\frac{g_{j_1}-g_{j_2}}{h_{j_1}-h_{j_2}}\)。
这里我们可以把 \(j\) 看成横坐标,\(g_j\) 看成纵坐标,这样右边的东西就可以看成是斜率。然后维护一个下凸壳。具体地,因为 \(2\times h_i\) 单调递增,所以采用一个单调队列维护斜率,每次当队头的斜率小于 \(2\times h_i\) 就弹出队头,然后使用队头对 \(i\) 转移,最后维护一下队尾的单调性,然后单调队列内加入 \(i\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,c,h[N],q[N],f[N];
double get(int i,int j){
return ((f[i]+h[i]*h[i])-(f[j]+h[j]*h[j]))*1.0/(h[i]-h[j]);
}
signed main(){
cin>>n>>c;
for(int i=1;i<=n;i++){
cin>>h[i];
}
int hh=0,tt=0;
q[tt]=1;
for(int i=2;i<=n;i++){
while(hh<tt&&get(q[hh],q[hh+1])<2*h[i])hh++;
f[i]=f[q[hh]]+(h[i]-h[q[hh]])*(h[i]-h[q[hh]])+c;
while(hh<tt&&get(q[tt-1],q[tt])>get(q[tt],i))tt--;
q[++tt]=i;
}
cout<<f[n];
return 0;
}