首页 > 其他分享 >dp套dp 随写

dp套dp 随写

时间:2024-06-13 16:33:19浏览次数:30  
标签:状态 int ll 内层 枚举 随写 dp

我不很理解为什么将这个东西拿出来单独讲。

这种题大概的模型就是内层来个相较于同等题目简单的内层 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

相关文章

  • LeetCode132双周赛T3,搜索和DP
    求出最长好子序列Idfs(i,j)表示以nums[i]结尾的,至多有j对相邻元素不同的最长序列的长度转移:枚举p<i,如果nums[p]!=nums[i],就从dfs(p,j-1)+1转移过来如果nums[p]==nums[i],就从dfs(p;j)+1转移过来classSolution{public:intmaximumLength(vector<int......
  • NLP实战入门——文本分类任务(TextRNN,TextCNN,TextRNN_Att,TextRCNN,FastText,DPCNN,BERT,ERN
    本文参考自https://github.com/649453932/Chinese-Text-Classification-Pytorch?tab=readme-ov-file,https://github.com/leerumor/nlp_tutorial?tab=readme-ov-file,https://zhuanlan.zhihu.com/p/73176084,是为了进行NLP的一些典型模型的总结和尝试。中文数据集从THUCNews......
  • 13. UDP协议与RTP协议
    UDP协议UDP协议比较简单:UDP的长度是固定的,用总长度-UDP长度就是数据长度。UDP是不保证他的有序性和可靠性的。对于音频和视频是这样是比较好的,因为这段丢了,我们可以从下一段在开始解码。RTPRTP协议概述RTP(Real-timeTransportProtocol)是用于Internet上针对多媒体......
  • DP经典问题----背包问题的代码实现(入门级)(C++/PYTHON)
    背包的状态转换方程i:表示物品序号j:表示背包大小W[i]:表示第i件物品的重量f[i,j]:表示在前i件物品中选择若干件放在承重为j的背包中,可以取得的最大价值f[i-1,j-Wi]:表示在前i-1件物品中选择若干件放在承重为j-Wi的背包中,可以取得的最大价值Pi(j>=Wi):表示第i件物品的价值,要......
  • [DP] DP优化总结
    写在前面$DP$,是每个信息学竞赛选手所必会的算法,而$DP$中状态的转移又显得尤为关键。本文主要从状态的设计和转移入手,利用各种方法对朴素$DP$的时间复杂度和空间复杂度进行优化与处理,以达到满足题目要求的目的;参考文献:动态规划算法的优化技巧毛子青c++DP总结《算......
  • 状压dp
    状压dp1.状态压缩状态压缩就是使用某种方法,以最小的代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要使用状态压缩的对象的点的状态只有两种:0和1。2.使用条件1.解法需要保存一定的状态数据(表示一种状态的一个数据值),每个状态通常情况下是可以用二进制......
  • [DP] DP优化总结
    写在前面$DP$,是每个信息学竞赛选手所必会的算法,而$DP$中状态的转移又显得尤为关键。本文主要从状态的设计和转移入手,利用各种方法对朴素$DP$的时间复杂度和空间复杂度进行优化与处理,以达到满足题目要求的目的;参考文献:动态规划算法的优化技巧毛子青c++DP总结《算......
  • [DP] [倍增优化] Luogu P1081 [NOIP2012 提高组] 开车旅行
    [NOIP2012提高组]开车旅行题目描述小\(\text{A}\)和小\(\text{B}\)决定利用假期外出旅行,他们将想去的城市从$1$到\(n\)编号,且编号较小的城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市\(i\)的海拔高度为\(h_i\),城市\(i\)和城市\(j\)之间的距......
  • DP(优化)
    史不分好坏。是史就应该冲进......
  • atcoder 官方dp题单题解(持续更新)
    题单链接:https://atcoder.jp/contests/dp/tasks洛谷搜索:https://www.luogu.com.cn/problem/list?keyword=at_dp&type=AT|B|CF|P|SP|UVA&page=1A题目链接:https://atcoder.jp/contests/dp/tasks/dp_a简单线性dp.dp[i]表示在i这个位置的最小代价,转移的时候把两种选择都考虑......