2023ACM暑假训练day 11 动态规划

DAY 11 动态规划

训练地址:传送门

训练情况简介

2023-07-10 09:30:17 星期一
早上:A
下午:BCR
晚上:AA
2023-07-11 15:33:28 星期二
早上:背包九讲回顾
下午:DE
晚上:看算法竞赛书

题目给我的感觉就是,这怎么能DP的啊?看了题解,我敲妙啊!呜呜呜~
DP问题更重要的是怎么构造状态,怎么转移状态
难,再理解理解!
开的这些题我好像不太能做,7.11晚上转算法竞赛书学习基础知识
这次训练的题,有空再补吧[ ] 2023-07-11 19:06:44 星期二
A 题 多维度的DP 想清楚每个维度的意义,初始态的确定和状态转移的确定。还需要再理解理解
B 题 二分答案+DP 问题在于怎么构造DP简化我们的思考和运算
C 题 树形DP 利用DFS求解DP问题
R 题 值域DP 在所给数的值域里进行DP求解问题
AA 题 计数DP入门
D 题 组合数学问题,需要一点点思考
E 题

A 题 题解出

cf 14 E. Camels
题意:
一行有n个空位,每个空位可以填\([1,4]\)的整数,相邻两个数不相同,要求:

  • 1.有\(t\)个位置满足 \(a_{i-1}\) < \(a_i\) >$a_{i+1} $并且(\(1\)<\(i\)<\(n\))
  • 2.有\(t\)\(1\)个位置满足\(a_{i-1}\)>\(a_i\)<\(a_{i+1}\)并且(\(1\)<\(i\)<\(n\))

思路:
关键在于状态的设计与状态的转移
开四维\(DP[i][now][num][flag]\),维度分别代表第i个位置,当前位置的数为now,到当前位置的波峰数num,到当前位置的波方向(0-下降,1-上升)
t个峰之间一定有t-1个谷,所以,t个峰的两侧,一定是下降的,即最左边的势态是递增,最右边的势态是递减。
具体思路见代码:

//>>>Qiansui
#include<map>
#include<set>
#include<list>
#include<stack>
#include<cmath>
#include<queue>
#include<deque>
#include<cstdio>
#include<string>
#include<vector>
#include<utility>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<functional>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

inline ll read()
{
	ll x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-48;ch=getchar();}
	return x*f;
}

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*
dp?问题在于怎么设计状态并且转移
*/
const int maxm=2e5+5,inf=0x3f3f3f3f,mod=998244353;
int n,t,ans,dp[25][5][15][2];

void solve(){
	cin>>n>>t;
	//初始状态
	for(int now=1;now<=4;++now){
		dp[1][now][1][1]=1;
	}
	for(int i=2;i<=n;++i){
		for(int num=1;num<=t;++num){
			for(int now=1;now<=4;++now){
				for(int pre=1;pre<=4;++pre){
					if(now>pre){
						dp[i][now][num][1]+=dp[i-1][pre][num][1]+dp[i-1][pre][num-1][0];
					}
					if(now<pre){
						dp[i][now][num][0]+=dp[i-1][pre][num][1]+dp[i-1][pre][num][0];
					}
					if(i==2){//最左端势态为递增,故递减态的状态数均为0
						dp[i][now][num][0]=0;
					}
				}
			}
		}
	}
	int sum=0;
	for(int now=1;now<=4;++now){
		sum+=dp[n][now][t][0];
	}
	cout<<sum<<'\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

相关资料:
https://blog.csdn.net/weixin_30871293/article/details/99932921


B 题 题解出

题意:
给你一个长为n的数组,你可以执行k次操作,每次操作你可以改变数组中的一个数使其变为任意的数。规定\(c(a)=max_{1\le i\le n-1}|a_{i+1}-a_i|,n>1;c(a)=0,0\le n\le 1\),你的目的就是在给定的操作次数内使得c(a)尽可能小
思路:
二分答案+DP。。。
我们可以利用二分来找最终的答案,二分一个mid,我们判断让整个数组的最大相邻差为mid所需要的次数是否符合条件即可。那么怎么判断呢?我们利用DP来辅助我们的判断。
设DP[i]表示第i个数不变,前i-1个数所需改变的最小次数使得mid为最大相邻差成立。设立初始态dp[1]=0,之后遍历在i前遍历数组,如果说\(|a_i-a_j|<=mid*(i-j)\),那么这个[j,i]区间是可以成立的,我们直接修改这个区间的所有元素使得该区间成立,同时取最小值更新dp[i]。更新完之后再判断一件事情,就是如果说我们强制更新所有i后的数,使得整体均成立的会有一个次数,我们判断这个次数是否满足\(dp[i]+n-i\le k\),如果满足,当前mid符合条件。更新到最后,如果\(dp[n]\le k\),当前mid符合条件。
那么最后,最后一件事情,我们是怎么找到,第i个位置前使得mid满足的最小更改次数的呢?这就是利用动态规划来完成的。从前到后,动态规划保证了每次取得最小,每次判断都是从头开始,实现了局部最优到全局最优的过程。妙!
下为代码:

//>>>Qiansui
#include<map>
#include<set>
#include<list>
#include<stack>
#include<cmath>
#include<queue>
#include<deque>
#include<cstdio>
#include<string>
#include<vector>
#include<utility>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<functional>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

inline ll read()
{
	ll x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-48;ch=getchar();}
	return x*f;
}

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=2e3+5,inf=0x3f3f3f3f,mod=998244353;
ll n,k,a[maxm],dp[maxm];

bool calc(ll mid){
	dp[1]=0;
	for(int i=2;i<=n;++i){
		dp[i]=i-1;
		for(int j=1;j<i;++j){
			if(abs(a[i]-a[j])<=mid*(i-j))
				dp[i]=min(dp[i],dp[j]+i-j-1);
		}
		if(dp[i]+n-i<=k) return true;
	}
	if(dp[n]<=k) return true;
	return false;
}

void solve(){
	cin>>n>>k;
	for(int i=1;i<=n;++i){
		cin>>a[i];
	}
	ll l=0,r=2e9,mid,ans;
	while(l<=r){
		mid=(l+r)>>1;
		if(calc(mid)){
			ans=mid;
			r=mid-1;
		}else l=mid+1;
	}
	cout<<ans<<'\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}s

相关资料:
https://blog.csdn.net/V5ZSQ/article/details/75530349


C 题 题解补

题意:
给你一棵有n个节点的树,下标从0开始。每个节点都有一个颜色,第i个节点可以为白色或黑色,由题目指定。
现在你可以从中删去若干条边,使得剩下的每个连通部分恰有一个黑色节点。
问有多少种符合条件的删边方法,答案对\(10^9+7\)取模。
思路:
树形DP
目前的问题在于,怎么计算分割?感觉不是特别容易,但其实从答案出发,我们直接统计方案数
对于一个父节点,本身如果是白的,想要满足条件,必须要与黑的子节点相连,或与白的子节点相连递归回上一个父节点;而如果本身是黑的,如果想要满足条件,就必须与黑的子节点断开,和白的子节点相连。
\(dp[u][0/1]\) 表示 \(u\) 点属于一个无黑点/有且仅有一个黑点的连通块时的方案数。
对于\(dp[u][1]\)\(dp[u][1]=dp[u][1]\times dp[v][0]+dp[u][1]\times dp[v][1]+dp[u][0]\times dp[v][1]\)
其中三项的转移情况分别是:(儿子不合理,贴到父亲上)+(父子都合理,中间划开)+(父亲不合理,贴到儿子上)
同理,对于 $ dp[u][0]$ :$ dp[u][0]=dp[u][0]\times dp[v][0]+dp[u][0]\times dp[v][1] $
即:(父子不合理)+(儿子合理,于是划开)
初始化为 $ dp[u][color[u]]=1 $

还是有点怪的感觉,再理解理解

详见相关资料
下为代码:

//>>>Qiansui
#include<map>
#include<set>
#include<list>
#include<stack>
#include<cmath>
#include<queue>
#include<deque>
#include<cstdio>
#include<string>
#include<vector>
#include<utility>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<functional>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

inline ll read()
{
	ll x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-48;ch=getchar();}
	return x*f;
}

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*
树形DP~
利用dfs做DP操作
关键还是在于怎么构造状态和状态转移
爆ll了?寄
链式前向星又忘记2倍空间了!(恼)
*/
const int maxm=1e5+5,inf=0x3f3f3f3f,mod=1e9+7;
ll n,cnt=1,head[maxm],ans,dp[maxm][2];

struct edge{
	int to,next;
}p[maxm<<1];

void add_edge(int a,int b){
	p[cnt].to=b;
	p[cnt].next=head[a];
	head[a]=cnt++;
	return ;
}

void dfs(int x,int fa){
	for(int i=head[x];i;i=p[i].next){
		int v=p[i].to;
		if(v==fa) continue;
		dfs(v,x);
		dp[x][1]=(dp[x][1]*((dp[v][1]+dp[v][0])%mod)%mod+dp[x][0]*dp[v][1]%mod)%mod;
		dp[x][0]=dp[x][0]*((dp[v][1]+dp[v][0])%mod)%mod;
	}
	return ;
}

void solve(){
	cin>>n;
	int a;
	for(int i=1;i<n;++i){
		cin>>a;
		add_edge(i,a);
		add_edge(a,i);
	}
	for(int i=0;i<n;++i){
		cin>>a;
		dp[i][a]=1;
	}
	dfs(0,-1);
	cout<<dp[0][1]<<'\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

相关资料:
https://blog.csdn.net/weixin_45080867/article/details/106046868


R 题

题意:
给你n个数,问你取给定数组中所有任意长的递增子序列的异或和的不同值有哪些?
思路:
这个是cf的easy version,有时间做做hard version
easy version中\(0\le a_i\le 500\),所以我们可以直接对作为DP对象。
令DP[i]表示序列异或和为i的末尾元素最小值,因为最小值更容易构造递增子序列,也相当于递增子序列的化简操作。之后遍历0~511,当\(dp[j]<a[i]\)时,构成递增子序列,可以更新异或的值:\(dp[j\otimes a[i]]=min(dp[j\otimes a[i]],a[i])\)。遍历整个数组即得到答案。
下为代码:

//>>>Qiansui
#include<map>
#include<set>
#include<list>
#include<stack>
#include<cmath>
#include<queue>
#include<deque>
#include<cstdio>
#include<string>
#include<vector>
#include<utility>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<functional>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

inline ll read(){
	ll x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-48;ch=getchar();}
	return x*f;
}

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=1e5+5,inf=0x3f3f3f3f,mod=998244353,N=512;
int n,a[maxm],dp[maxm];

void solve(){
	mem(dp,inf);
	cin>>n;
	int t;
	dp[0]=0;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		for(int j=0;j<N;++j){
			if(dp[j]<a[i]){
				dp[j^a[i]]=min(dp[j^a[i]],a[i]);
			}
		}
	}
	vector<int> ans;
	for(int i=0;i<N;++i){
		if(dp[i]!=inf){
			ans.push_back(i);
		}
	}
	cout<<ans.size()<<'\n';
	for(auto x:ans){
		cout<<x<<' ';
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

AA 题

题意:
规定a数组的大小为n,\(1\le a_i\le m\),且\(\sum_{i=1}^{n}{a_i}\le k\),问你符合条件的a数组的个数取模998244353
思路:
利用DP求解。为什么能用?因为如果说我们已经确定了数组的一部分,那么我们只关心已经实现部分数组的和以及相应的方案数,而不关心具体的数字如何安排,所以可以用DP求解
\(DP[i][j]\)表示数组从前i部分的和为j
那么,我们可以遍历整个数组,每个数j遍历 1~m,sum遍历前一位的和 0~k,当 j+sum<=k 时由前一位向后更新,最后求和\(\sum_{i=n}^{k}{dp[n][i]}\)即为答案
下为代码:

//>>>Qiansui
#include<map>
#include<set>
#include<list>
#include<stack>
#include<cmath>
#include<queue>
#include<deque>
#include<cstdio>
#include<string>
#include<vector>
#include<utility>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<functional>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

inline ll read(){
	ll x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-48;ch=getchar();}
	return x*f;
}

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=50+5,inf=0x3f3f3f3f,mod=998244353;
ll n,m,k,dp[maxm][maxm*maxm];

void solve(){
	cin>>n>>m>>k;
	dp[0][0]=1;
	for(int i=0;i<n;++i){
		for(int d=1;d<=m;++d){
			for(int j=0;j+d<=k;++j){
				dp[i+1][j+d]=(dp[i+1][j+d]+dp[i][j])%mod;
			}
		}
	}
	ll ans=0;
	for(int i=n;i<=k;++i){
		ans=(ans+dp[n][i])%mod;
	}
	cout<<ans<<'\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

D 题

题意:
给 n 个有颜色的球,装在一个袋子里,颜色的种数为 k 种,每种颜色的球的数量给定。
现在从袋子里开始取球,对于所有的颜色 i ~\([1,k-1]\),均存在一个要求:第 i+1 种颜色的球的最后一个球在第 i 个球的最后一个球取出之后才被取出。问你总的方案数,取模1e9+7
思路:
组合数学
正向考虑取球不容易,我们考虑逆向取球。先将最后一种颜色的最后一个球放在最后,设最后一种球共有 k 个,未放球的位置还有 n 个,那么剩下的球有\(C_{n-1}^{k-1}\)种放置方法;再考虑倒数第二个球,设倒数第二种球共有 t 个,未放球的位置还有 n-k 个,则倒数第二种球的摆放方法还有\(C_{n-k-1}^{t-1}\)种...
则最终的答案即为所有的组合数相乘取余的结果
!从后往前即不用考虑题目所给定的要求,因为已经默认包含在里面了。还有就是先放置的球占据位置后,可以将这些位置直接不考虑,相当于每次放球都是从最后开始放,只不过总共能放置的位置变化,这个利用动态更新数组和即可
下为代码:

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

inline ll read(){
	ll x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-48;ch=getchar();}
	return x*f;
}

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*
wa3?
ans=(ans*c[sum-1][n[i]-1])%mod;这里组合数后面的n[i]-1之前写错了。。。
样例刚好过了。。。
*/
const ll maxm=1e3+5,inf=0x3f3f3f3f,mod=1e9+7;
int k,n[maxm];
ll c[maxm][maxm];

void pre(){//预处理组合数,利用了组合数的递推公式
	for(int i=1;i<=1000;++i){
		c[i][i]=c[i][0]=1;
		for(int j=1;j<i;++j){
			c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
		}
	}
	return ;
}

void solve(){
	pre();
	cin>>k;
	ll sum=0,ans=1;
	for(int i=1;i<=k;++i){
		cin>>n[i];
		sum+=n[i];
	}
	for(int i=k;i>0;--i){
		if(n[i]==1){
			--sum;
			continue;
		}
		ans=(ans*c[sum-1][n[i]-1])%mod;
		sum-=n[i];
	}
	cout<<ans<<'\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

相关资料:
https://blog.csdn.net/amazingee/article/details/104974181

E 题 未出,题解补

题意:
给定一个长度为 n 的序列,请你变换它,使得\(\sum|a[i]-a[i+k]|\)最小,输出这个最小的\(\sum|a[i]-a[i+k]|\)
思路:
详见相关资料
相关资料:
https://blog.csdn.net/qq_43857314/article/details/106997532
https://blog.csdn.net/Rising_shit/article/details/78989379

题意:

思路:

posted on 2023-07-10 09:30  Qiansui  阅读(6)  评论(0编辑  收藏  举报