我不很理解为什么将这个东西拿出来单独讲。
这种题大概的模型就是内层来个相较于同等题目简单的内层 dp,再在外面套个壳子, 比如说每个元素给个取值范围而非定值。一种不恰当的类比是函数复合。
整体思路就是你需要先设计出一个内层 dp,然后把内层地转移看成一个类似于自动机的图,外层 dp 就是把内层 dp 的结果拿出来当状态再做一次转移,往往这种时候状态数是爆炸的,就需要在外层去优化/剪枝不必要的转移/状态。
所以难点就在两个方向上,一个是设计出内层的 dp,一个是优化外层的状态和转移。
这种套娃题往往是可以出的极难无比的,出题人可以不断地将内层dp 的难度提高,随之而来的就是外层的设计越来越复杂,你去化简外层dp 的难度(尤其是代码难度)会飞速提升。
值得注意的是,外层 dp 的优化是有万能的算法去解决的,即 Hopcroft DFA 最小化算法 - yyyyxh ,但是由于算法难度/代码难度较高,在 OI 中并不常用(dp 套 dp 本身出的也不多),有兴趣可以自行了解。
P4590 [TJOI2018] 游园会
首先你先忽略到存在 \(NOI\) 字串的限制,不妨设给定的串为 \(T\),你用的串为 \(S\)。
考虑确定了 \(S\),就是典中典。设 \(f_{i,j}\) 表示考虑 \(S\) 中前 \(i\) 个 ,\(T\) 中前 \(j\) 个字符的最大公共子序列。
那么有转移方程:
\[f_{i,j}=\max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}+(S_i=T_j)) \]内层 dp 做完了,考虑外层。
注意到 \(|T|\) 很小,且 \(f_{i,x}\) 的转移只和 \(f_{i-1,y}\) 以及 \(f_{i,z}\) 相关。
考虑一层一层转移,把所有的可能的 \(f_{i-1,x}\) 一维数组看成一个状态(自动机上的一个节点),把刚才的方程当作自动机转移条件,那么每次都将枚举所有状态,根据转移条件去暴力枚举。
具体而言,假设已经知道了所有 \(f_{i-1}\) 的状态,考虑去枚举第 \(S_i\) 是什么字符,然后去更新所有 \(f_i\) 的状态。
外层 dp 设计完了,但是状态数太多了,毛估估下大概有 \(O(n\times k^k)\) 个状态,每次转移是 \(O(k)\) 的,爆了。
定睛一看,你发现很多状态是无用的啊,具体而言,你注意到 \(f_{i,j}\le f_{i,j+1}\le f_{i,j}+1\)
差分一手后数组每一个值只能是 \(0/1\),那么对于一个 \(f_i\) ,那么有用的状态为 \(O(n\times 2^k)\)。能接受。
还剩个问题就是不能出现 \(NOI\),考虑再加维度 \(0/1/2\) 表示已经匹配到了 \(NOI\) 的 \(0/1/2\) 个值了,这一步不困难,可结合代码理解。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=17,M=(1<<15)+3,H=1e9+7;
int n,m,a[N],ans[N],to[3][3]={{1,0,0},{1,2,0},{1,0,3}},h[2][N],w[M][3],f[M][3],g[M][3];
char ch[N];
void Init()
{
for(int S=0;S<(1<<m);S++)
{
for(int i=1;i<=m;i++)h[0][i]=h[0][i-1]+(S>>(i-1)&1);
for(int k=0;k<=2;k++)
{
for(int i=1;i<=m;i++)
{
h[1][i]=max(h[1][i-1],h[0][i]);
if(a[i]==k)h[1][i]=max(h[1][i],h[0][i-1]+1);
}
for(int i=1;i<=m;i++)if(h[1][i]>h[1][i-1])w[S][k]|=1<<(i-1);
}
}
}
void Add(int &x,int y){x=x+y>=H?x+y-H:x+y;}
int main()
{
cin>>n>>m>>(ch+1);
for(int i=1;i<=m;i++)a[i]=ch[i]=='N'?0:ch[i]=='O'?1:2;
Init();f[0][0]=1;
for(int i=1;i<=n;i++)
{
swap(f,g);memset(f,0,sizeof(f));
for(int S=0;S<(1<<m);S++)for(int j=0;j<=2;j++)for(int k=0;k<=2;k++)
if(j<2||k<2)Add(f[w[S][k]][to[j][k]],g[S][j]);
}
for(int S=0;S<(1<<m);S++)for(int j=0;j<=2;j++)Add(ans[__builtin_popcount(S)],f[S][j]);
for(int i=0;i<=m;i++)cout<<ans[i]<<endl;
}
P8352 [SDOI/SXOI2022] 小 N 的独立集
还是先设计内层状态,设 \(g_{i,0/1}\) 表示子树 \(i\) 内是不选/选子树根的最大值。
外层的话直接设 \(f_{x,i,j}\) 表示子树 \(x\) 内不选/选子树根,最大值分别为 \(i,j\) 的方案数。
考虑树形 \(dp\) 合并子树 \(y\),有转移:
\[f'_{x,i+max(k,l),j+k}\leftarrow f'_{x,i+max(k,l),j+k}+f_{x,i,j}\times f_{y,k,l} \]复杂度 \(O(n^4 k^4)\),爆了,去掉零状态依旧爆爆爆。
可是外层看着没啥能优化了,于是乎回到内层重新考虑,设 \(g_{i,0/1}\) 表示在 \(i\) 子树内不(0)/要(1)强制不选根节点的最大值,转移方程和原本的类似,不细说。
这种 dp 的方式好在哪里呢?\(0\le g_{i,0}-g_{i,1}\le k\),最多就是差个 \(i\) 的权值。
重新设状态 \(f_{x,i,j}\) 表示子树中 \(g_{u,0/1}\) 分别为 \(i+j,i\) 的方案数。合并子树 \(y\) 有转移:
\[f'_{x,i+k+l,max(j,l)-l}\leftarrow f'_{i+k+l,max(j,l)-l}+f_{x,i,j}\times f_{y,k,l} \]复杂度 \(O(n^2k^4)\),把零状态去掉跑得飞快。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e3+3,H=1e9+7;
ll n,k,sz[N],f[N][N*5][6],tmp[N*5][6];
vector<ll>ve[N];
void Add(ll &x,ll y){x=(x+y)%H;}
void Dfs(int x,int fa)
{
sz[x]=1;
for(int i=1;i<=k;i++)f[x][0][i]=1;
for(int y:ve[x])if(y!=fa)
{
Dfs(y,x);memset(tmp,0,sizeof(tmp));
for(int i=0;i<=k*sz[x];i++)for(int j=0;j<=k;j++)if(f[x][i][j])
for(int p=0;p<=k*sz[y];p++)for(int q=0;q<=k;q++)if(f[y][p][q])
Add(tmp[i+p+q][max(j,q)-q],f[x][i][j]*f[y][p][q]);
sz[x]+=sz[y];memcpy(f[x],tmp,sizeof(f[x]));
}
}
int main()
{
cin>>n>>k;
for(int i=1,x,y;i<n;i++)
cin>>x>>y,ve[x].push_back(y),ve[y].push_back(x);
Dfs(1,0);
for(ll i=1;i<=n*k;i++)
{
ll ans=0;
for(int j=0;j<=min(i,k);j++)Add(ans,f[1][i-j][j]);
cout<<ans<<endl;
}
}
P8497 [NOI2022] 移除石子
足够困难的题目,考虑内层dp 设计即对于固定状态的设计。
为了不变量混用,将题面中的 \(k\) 设成 \(m\),接下来的 \(k\) 都是变量。
最特殊的情况,\(m=0\)。
注意到一操作肯定是留来收尾的,主要的重点在于二操作。
设 \(a_i\) 表示在当前固定状态下第 \(i\) 堆的棋子数量。
考虑类似扫描线一样进行 dp,设 \(f_{i,x,y}\) 表示当前考虑了所有二操作左端点小于 \(i\) 的操作,钦定了有 \(j\) 个二操作已经延展到了 \(i\),可以选择继续向右延展或在 \(i\) 不动了,有 \(k\) 个二操作一定要延展到 \(i+1\) 是否可行。
转移考虑枚举有多少个二操作左端点在 \(i\),设其有 \(st\) 个。
那么能继续转移当且仅当 \(a_i-j-k-st\) 大于 \(0\) 且不为 \(1\),然后再去枚举 \(nj\) 表示在第 \(i+1\) 堆为右端点的操作二数量,那么需满足 \(k\le nj\le k+j\),那么 \(f_{i+1,nj,st}\) 也可行。
最后只需判断 \(f_{n+1,0,0}\) 是否可行即可。
注意到二操作操作的长度不会超过 \(5\),长度超过 \(5\) 的操作可以划分成若干短的操作拼接。
那么 \(j\le 6,k\le 3\),也就保证了我们枚举量的上界。
考虑加上 \(m\) 的限制,这个恰好放 \(m\) 个石头是烦的,大胆猜测添加小于 \(m\) 个球有解,那么添加 \(m\) 个球也有解,随便手玩一下发现只有两个反例,一个是全零,一个是 \(n=3\) 且全局为 \(0\)。
把这两种情况先判一手。
那么设 \(g_{i,j,k}\) 表示使得 \(f_{i,j,k}=1\) 至少需要加的石子数,还是去枚举 \(st,nj\),设 \(v=a_i-j-k-st\),如果 \(v<0\) 则需加 \(x=-v\) 个,否则需要添加 \(x=[v=1]\) 个,用 \(g_{i,j,k}+x\) 去更新 \(g_{i+1,nj,st}\) 即可。
最后检查 \(g_{n+1,0,0}\le k\) 即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e3+3,H=1e9+7;
void Min(int &x,int y){x=x<y?x:y;}
int n,m,ans,a[N],l[N],r[N],f[N][3][3];
bool Chk()
{
if(m==1&&(count(a+1,a+n+1,0)==n||(n==3&&a[1]==1&&a[2]==1&&a[3]==1)))return 0;
memset(f,0x3f,sizeof(f));f[1][0][0]=0;
for(int i=1;i<=n;i++)for(int j=0;j<=2;j++)for(int k=0;k<=2;k++)if(f[i][j][k]<=m)
{
for(int st=0;st<=2;st++)
{
int v=a[i]-j-k-st,x=v<0?-v:v==1;
for(int nj=k;nj<=min(2,j+k);nj++)Min(f[i+1][nj][st],f[i][j][k]+x);
}
}
return f[n+1][0][0]<=m;
}
void Dfs(int x)
{
if(x==n+1){ans+=Chk();return;}
for(int i=l[x];i<=r[x];i++)a[x]=i,Dfs(x+1);
}
void Solve()
{
cin>>n>>m;ans=0;
for(int i=1;i<=n;i++)cin>>l[i]>>r[i];
Dfs(1);cout<<ans<<endl;
}
int main()
{
int T;cin>>T;
while(T--)Solve();
}
此时可以获得 \(40\) 分的高分了,在当年你已经吊打了至少百分之九十的选手了。
等等,为什么你的 \(j,k\) 枚举量只有 \(2\) 呢?
注意到这题是 dp 套 dp 的模型,这种常数的枚举变化往往在外层的优化是显著的,所以在设计出内层 dp 后一定要多看看有没有什么优化的空间?
怎么去优化的呢?
考虑拿着一开始未优化的代码一点一点的去减少这种枚举的上界然后去对拍,拍个很多组没锅大概就是对的了。
其实你也可以去手动大力分讨去减少枚举上界,可是只要出现一步的粗心或者漏算你就爆了。
然后考虑设计外层 dp。
首先你能感受到 \(a_i\ge 8\) 之后它的转移大概是本质相同的,你考虑你预处理一下 \(a_i\) 在不同取值能走到的状态,实际情况下 \(a_i\ge 6\) 之后转移就是一样的了。
注意到 \(f_i\) 只有九种不同的状态,考虑把他们压一手,看似 \(g_{i,j,k}\) 有 \(0\sim 101\) 这 \(102\) 种取值,所以看上去要爆了,但是实际上去写个搜索预处理状态之后,你会发现只有 \(8765\) 个本质不同的状态,非常的牛牛啊。然后你随便写写就通过本题了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e3+3,M=8765,H=1e9+7;
ll n,m,tot,l[N],r[N],tr[M][7],f[M],g[M];
map<vector<ll>,int>mp;
void Add(ll &x,ll y){x=x+y>=H?x+y-H:x+y;}
void Min(ll &x,ll y){x=x<y?x:y;}
int Dfs(vector<ll> cur)
{
if(mp.count(cur))return mp[cur];
int id=mp[cur]=tot++;
for(int t=0;t<=6;t++)
{
vector<ll>now(9,101);
for(int j=0;j<=2;j++)for(int k=0;k<=2;k++)if(cur[j*3+k]!=101)
{
for(int st=0;st<=2;st++)
{
int v=t-j-k-st,x=v<0?-v:v==1;
for(int nj=k;nj<=min(2,j+k);nj++)Min(now[nj*3+st],cur[j*3+k]+x);
}
}
tr[id][t]=Dfs(now);
}
return id;
}
ll Fix()
{
if(m!=1)return 0;
return H-(count(l+1,l+n+1,0)==n)-(n==3&&l[1]<=1&&1<=r[1]&&l[2]<=1&&1<=r[2]&&l[3]<=1&&1<=r[3]);
}
void Solve()
{
cin>>n>>m;memset(f,0,sizeof(f));f[0]=1;
for(int i=1;i<=n;i++)cin>>l[i]>>r[i];
for(int i=1;i<=n;i++)
{
memcpy(g,f,sizeof(g));memset(f,0,sizeof(f));
for(int j=0;j<tot;j++)if(g[j])for(int t=0;t<=6;t++)
{
ll x=0;
if(t<6)x=l[i]<=t&&t<=r[i];
else if(r[i]>=6)x=r[i]-max(6ll,l[i])+1;
Add(f[tr[j][t]],g[j]*x%H);
}
}
ll ans=Fix();
for(auto it:mp)if(it.first[0]<=m)Add(ans,f[it.second]);
cout<<ans<<endl;
}
int main()
{
vector<ll>ve(9,101);ve[0]=0;Dfs(ve);
int T;cin>>T;
while(T--)Solve();
}
P5279 [ZJOI2019] 麻将
你知道的,我一直都是吉老师的粉丝,至于九条可怜,我祝他一切安好。
逆天题,有兴趣自行了解。
完结撒花
标签:状态,int,ll,内层,枚举,随写,dp From: https://www.cnblogs.com/Hanghang007/p/18246202