专题-DP

前言:

激动的心,颤抖的手,菜鸡又来水博客惹~~ 一天做了贺了四道题,真充实啊!!┭┮﹏┭┮

\(T1:\)CF1970G2 Min-Fund Prison (Medium)

思路:

题目要求\(x^2+y^2+k×c\)的最小值:首先c是一定的,所以不用考虑。对于\(k\)来说,我们知道每连一条割边就会减少一个连通块,所以\(k\)的值为连通块的数量\(-2\)。然后不固定的就只剩\(x,y\)了,但\(x,y\)的和也是定值,所以我们就可以开心的枚举啦~~

首先建图肯定是不用多说的。然后看眼数据范围,嗯,比较友好。那我们就枚举每一条连接\(x,y\)的边,那么此时,这条边判为两种情况:要么这条边是本来就存在在图上的;要么是我们后来给它加上的。对于原本就存在的边,我们只需要给它直接删除就好;对于原本没有的边,我们要先给这条边加上,然后再给它删除就好啦~~

\(ps:\)打码中的\(f[i]\)表示把图分为大小为\(i\)\(n-i\)的两个连通块是符合情况的。

代码:

#include<iostream>
#include<bitset>
#include<vector>
#include<cstring>
#define int long long
using namespace std;
const int N=305;
int T,n,m,c,x,y,num,tot,ans=1e18;bool a[N][N],vis[N];bitset<N> f;vector<int> siz,v[N];
inline void init(){
	ans=1e18;
	memset(a,0,sizeof(a));
	for(int i=1;i<=n;i++) v[i].clear();
}//多组数据清空 
inline void dfs(int x){
	if(vis[x]) return ;
	vis[x]=1;
	num++;
	for(int now:v[x]) if(a[x][now]) dfs(now);
}//搜索连通块的大小 
signed main(){
	ios::sync_with_stdio(false);
	cin>>T;
	while(T--){
		cin>>n>>m>>c;
		init();	
		for(int i=1;i<=m;i++){
			cin>>x>>y;
			v[x].push_back(y);v[y].push_back(x);//建边(不建议用链式前向星,后面处理起来不太方便【也可能是我菜,那就当我没说】) 
			a[x][y]=a[y][x]=1;//标记边存在 
		}
		for(int x=1;x<n;x++){
			for(int y=x+1;y<=n;y++){//遍历x,y 
				int k=a[x][y]^1;//记录本条边是否存在方便后面回溯 
				bool tmp=a[x][y];tot=0;
				if(k==1){//不存在 
					v[x].push_back(y);v[y].push_back(x);
					a[x][y]=a[y][x]=1; 
				}//加边
				memset(vis,0,sizeof(vis));//清空 
				for(int i=1;i<=n;i++) if(!vis[i]) tot++,dfs(i);//tot统计连通块的数量 
				if(k==1){
					v[x].pop_back();v[y].pop_back();
					a[x][y]=a[y][x]=0;
				}//再把边删除 
				a[x][y]=a[y][x]=0;
				siz.clear();
				memset(vis,0,sizeof(vis));
				for(int i=1;i<=n;i++) if(!vis[i]) num=0,dfs(i),siz.push_back(num);//储存连通块的大小 
				if(siz.size()>tot){//删边后连通块数量增多,即删除的是割边 
					k+=siz.size()-2;
					f.reset();//清空 
					f[0]=1;//初始化 
					int l=siz.size();
					for(int i=1;i<=l;i++) f|=(f<<siz[i]);//可能出现的情况 
					for(int i=0;i<=n;i++) if(f[i]) ans=min(ans,i*i+(n-i)*(n-i)+k*c);//统计答案 
				}
				a[x][y]=a[y][x]=tmp;//回溯 
			}
		}
		if(ans==1e18) cout<<-1<<'\n';//判无解 
		else cout<<ans<<'\n';//输出答案~~ 
	}
	return 0;
}

\(T2:\) CF1917F Construct Tree

思路:做了 晚上再敲思路 先粘没有注释的代码

代码~~:

#include<iostream>
#include<bitset>
using namespace std;
const int N=2050;
int T,n,l[N],d,maxn,maxx;bool g,h;
bitset<2050> f[2050],o;
int main(){
	ios::sync_with_stdio(false);
	cin>>T;
	while(T--){
		maxn=maxx=0;
		cin>>n>>d;
		for(int i=1;i<=n;i++) cin>>l[i],maxn=max(l[i],maxn);
		for(int i=0;i<=d;i++){
			o[i]=0;
			for(int j=0;j<=d-i;j++) f[i][j]=0;
		}f[0][0]=1;o[0]=1;h=0;
		for(int i=1;i<=n;i++){
			for(int j=d;j>=0;j--){
				f[j]|=(f[j]<<l[i]);
				if(j>=l[i]) f[j]|=f[j-l[i]];
			}
			if(h==1||l[i]!=maxn) o|=(o<<l[i]),maxx=max(maxx,l[i]);
			else h=1;
		}
		g=o[d-maxn];
		for(int i=0;i<=d;i++) if(i>=maxn&&d-i>=maxn&&f[i][d-i]==1) g=1;
		if(g==1&&maxn+maxx<=d) cout<<"Yes"<<'\n';
		else cout<<"No"<<'\n';
	}
	return 0;
}

\(T3:\)CF1914G2 Light Bulbs (Hard Version)

思路:

先奉上一位讲的超清晰的大佬

然后开始我的讲解啦:这个题目分为两个子任务。

任务一:
由题意及样例可知,如果一个大区间包含一个小区间,那么小区间不必被点亮。如果两个区间相互交叉,则点亮其中任意一个即可。所以,题目所求不过是不相交区间的个数罢了。

任务二:
任务二的烧烤量就比较大了:让求集合\(S\)的数量。

有任务一我们可以分析出,若干相交的区间只要有一个灯被点亮那其余的的灯就都都会被点亮,这些区间是对答案有贡献的区间。而被包含的的区间则是对答案没有贡献的区间。所以我们可以遍历每一个长区间(有包含关系的区间和相互交叉的区间),然后跳过其中被包含的闭合小区间,算出其他长区间内数字的个数,在将几个长区间内的答案相乘,就求出最终答案了!!!

代码:

#include<iostream>
#include<map>
#include<random>
#define int long long
using namespace std;
const int N=4e5+5,mod=998244353;
int T,n,c,ans1,ans2,cur[N],w[N];map<int,int> last;
mt19937_64 rnd(random_device{}());
inline int get(){
	int x=0;
	while(!x) x=rnd();
	return x;
}//随机数 
signed main(){
	ios::sync_with_stdio(false);
	cin>>T;
	while(T--){
		cin>>n;
		for(int i=1;i<=2*n;i++) w[i]=get();//随机化哈希 
		last.clear();
		ans1=0;ans2=1;//多组数据 
		for(int i=1;i<=2*n;i++){
			cin>>c;
			cur[i]=cur[i-1]^w[c];//异或哈希 
			last[cur[i]]=i;//记录该哈希值出现的最后一个位置,即跳过所有包含的闭合小区间后的位置 
			if(!cur[i]) ans1++;//不想交区间的个数 
		}
		for(int i=0;i<2*n;i++){//遍历 
			if(cur[i]) continue;//从区间端点开始 
			int j=i+1,res=1;
			while(cur[j]){//跳过中间闭合的小区间,即跳过被包含的区间 
				j=last[cur[j]]+1;
				res++;//其余区间都有贡献 
			}
			ans2=(ans2*res)%mod;//乘积
		}
		cout<<ans1<<" "<<ans2<<'\n';//输出答案~~ 
	}
	return 0;
}

\(T4:\) CF1906H Twin Friends

思路:

首先题目要求:如果至少有一个昵称不同,则认为两个昵称对不同。 所以我们需要把哥哥昵称的方案数与弟弟昵称的方案数相乘。哥哥昵称的方案数很好求,即为\(A\)的全排列数,即为\(n!\)。接下来我们需要考虑弟弟昵称的方案数。这里我们就需要使用\(dp\)了:首先有一维肯定表示一下我们遍历到哪了,又由题可知排列顺序待定,所以我们不妨按字母进行遍历。又因为题目说可以使用字母表的下一个字母,所以第二维我们设已经用了\(j\)个下一位字母。所以,\(dp[i][j]\)表示当前遍历到地\(i\)个字母,使用了\(j\)个下一个字母。易得递推方程\(f[i][j]=C_{a[i]}^j* \sum_{k=0}^{b[i]+j-a[i]}f[i-1][k].\)
所以,最后的答案为\(f[26][0]*\frac{n!}{Πa[i]!}\).

代码:

#include<iostream>
#define int long long
using namespace std;
const int N=200000+5,mod=998244353;
int m,n,ans,cnt1[30],cnt2[30],sum[N],f[30][N],power[N];string s,t;
inline int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1) res=(res*x)%mod;
		x=(x*x)%mod;
		y>>=1;
	}
	return res;
}//快速幂 
inline int inv(int x){
	if(x) return qpow(x,mod-2);
	else return 1;
}//求逆元 
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>m>>s>>t;
	for(int i=0;i<n;i++) cnt1[s[i]-'A'+1]++;//统计哥哥昵称中每个字符出现的次数  
	for(int i=0;i<m;i++) cnt2[t[i]-'A'+1]++;//统计弟弟昵称中每个字符出现的次数 
	power[0]=1;for(int i=1;i<=m;i++) power[i]=(power[i-1]*i)%mod;//预处理阶乘 
	f[0][0]=1;ans=power[n];
	for(int i=1;i<=26;i++){
		sum[0]=f[i-1][0];//前缀和 
		ans=(ans*inv(power[cnt1[i]]))%mod;//除以a[i]的阶乘 
		for(int j=1;j<=cnt2[i];j++) sum[j]=(sum[j-1]+f[i-1][j])%mod;//前缀和 
		for(int j=cnt1[i]-cnt2[i];j<=min(cnt1[i],cnt2[i+1]);j++) 
			f[i][j]=power[cnt1[i]]*inv(power[j])%mod*inv(power[cnt1[i]-j])%mod*sum[cnt2[i]+j-cnt1[i]]%mod;//套公式 
	}
	ans=(ans*f[26][0])%mod;//求出最终答案 
	cout<<ans<<'\n';//完结撒花~~ 
	return 0;
}

\(T5:\)CF1905E One-X

详情见另一篇博客

\(T6:\) CF1874C Jellyfish and EVA

思路:还没做。。

\(T7:\)CF1870E Another MEX Problem

思路:

我们设 \(f_{i,j}\) 表示前 \(i\) 个数选出若干子段后能否得到 \(j\).易得转移方程为\(f_{i,j}|=f_{k−1,j⊕mex(k,i)}.\)此时的时间复杂度是\(O(n^3)\)的,不能接受,因此我们考虑优化。易得结论有效区间的数目小于等于\(2n\)个(证明见下),那么我们只需要遍历有效区间即可,此时我们的时间复杂度为\(O(n^2)\)。便可以通过此题啦~~

证明:

简单的反证法可以证明,假设每个点作为区间两端的中较大的一个(避免重复计算),最多只有 \(2\)
个没法更小的区间(向左、向右各一个),共 \(2×n\) 个。

代码:

#include<iostream>
#include<vector>
using namespace std;
const int N=5050;
int T,n,ans,a[N],mex[N][N];bool f[N][N],flag[N];vector<int> l[N];
int main(){
	ios::sync_with_stdio(false);
	cin>>T;
	while(T--){
		cin>>n;ans=0;
		for(int i=1;i<=n;i++) cin>>a[i],l[i].clear();
		for(int i=1;i<=n;i++) for(int j=0;j<=5005;j++) f[i][j]=0;f[0][0]=1;//初始化 
		for(int i=1;i<=n;i++){
			int mx=0;
			for(int j=0;j<=5005;j++) flag[j]=0; 
			for(int j=i;j<=n;j++){
				flag[a[j]]=1;
				while(flag[mx]) mx++;
				mex[i][j]=mx;
			}
		}//先暴力预处理出区间mex值 
		for(int i=1;i<=n;i++) for(int j=0;j<=5005;j++) if(mex[i][j]!=mex[i+1][j]&&mex[i][j]!=mex[i][j-1]) l[j].push_back(i);//好区间 
		for(int i=1;i<=n;i++){
			for(int j=0;j<=5000;j++){
				f[i][j]|=f[i-1][j];
				for(int  k:l[i]) if((j^mex[k][i])<=5000) f[i][j]|=f[k-1][j^mex[k][i]];
			}
		} 
		for(int i=5001;i>=0;i--){
			if(f[n][i]){
				ans=i;
				break;
			}//遍历答案 
		}
		cout<<ans<<'\n';//完结撒花~~ 
	}
	return 0;
}

\(T8:\)CF1868C Travel Plan

思路:也没做。。。

\(T9:\)CF1866M Mighty Rock Tower

思路:

不会的题先推大佬题解

按理来说我们设的状态应该为\(f_i\)表示叠到\(i\)层的期望次数。但是稍微考虑一下,我们就会发现这个转移方程非常难想。所以我们考虑换一种状态,对原状态进行差分:设\(f_i\)表示从第\(i-1\)层叠到第\(i\)层的期望次数。

此时分三种情况:直接叠上去,坍塌到底层,坍塌了\(j\)层。其中第一种的期望值为1;第二种的期望值为\(p_i^i\sum_{j=1}^if_i\)(概率是\(p_i^i\),代价是\(\sum_{j=1}^if_i\));第三种的期望值为\(\sum_{j=1}^{i-1}(p_i^{j}(1-p_i)\sum_{k=i-j+1}^if_k)\)(概率是\(p_i^j(i-p_i)\)代价是\(\sum_{k=i-j+1}^if_k\))。

直接对三种情况线性相加,即:

\(f_i=1+p_i^i\sum_{j=1}^if_j+\sum_{j=1}^{i-1}(p_i^j(1-p_i)\sum_{k=i-j+1}^if_k)\)

\(\ \ =p_if_i+1+\sum_{j=2}^ip_i^jf_{i-j+1}\)

解得

\(f_i=\frac{1+\sum_{j=2}^ip_i^jf_i-j+1}{1-p_i}\)

方程化简到此为止,但此时的时间复杂度为\(O(n^2)\),显然会超时,所以我们考虑用前缀和优化\(sigma\)。因为\(p\)是不定的,但是\(p\)的取值仅限于\(1\)~\(99\)之间,所以我们用每一种\(p\)去求前缀和。设\(s_i=\sum_{j=2}^ip_i^jf_i-j+1\),那么\(s_{i+1}=p(s_i+p*f_i)\)。这样,总的时间复杂度就位\(O(n)\),常数为100,略大,但能过。

代码:

#include<iostream>
#define int long long
using namespace std;
const int N=2e5+5,mod=998244353,inv=828542813;
int n,p[105],ans,a[N],dp[N],s[105];
inline int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1) res=(res*x)%mod;
		x=(x*x)%mod;
		y>>=1;
	}
	return res%mod;
}//快速幂 
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=0;i<100;i++) p[i]=i*inv%mod;//求出概率 
	for(int i=1;i<=n;i++){
		dp[i]=(s[a[i]]+1)*qpow(1-p[a[i]]+mod,mod-2)%mod;//递推方程式 
		ans=(ans+dp[i])%mod;//求答案 
		for(int j=0;j<100;j++) s[j]=(s[j]*p[j]%mod+p[j]*p[j]%mod*dp[i])%mod;//每一个p都算一便 
	}cout<<ans<<'\n';//输出答案 
	return 0;//完结撒花~~ 
}//借鉴自:https://www.luogu.com.cn/article/iwh4skdn

\(T10:\)P10599 BZOJ2164 采矿

思路:还没做。。。。

posted @ 2025-08-19 22:05  晏清玖安  阅读(18)  评论(0)    收藏  举报