杭电多校2022 dp题范做

博客围绕多场算法题展开,涉及背包、Boss战斗、括号序列等问题。运用动态规划思想解题,如背包问题用逆向思考和bitset记录可行容量集合;Boss战斗问题结合状态压缩和二分法;括号序列问题采用区间dp等,还给出各题的具体思路和转移方程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一场 Backpack

描述:
给定背包容量为m,给定n物品,每个物品的容量为w[i],价值为v[i],现在要从n个物品中选出若干物品,要求恰好把背包填满,并且要求这些物品的价值异或值最大,问能得到的最大价值异或值,如果不能恰好把背包填满输出-1
思路:
逆向思考,定义dp数组为 b i t s e t < 1024 > d p [ i ] [ j ] bitset<1024>dp[i][j] bitset<1024>dp[i][j],代表到了前面i个物品,目前物品的价值异或和为j时,有哪些背包容量可行,用bitset记录可行的容量集合
如果选择第i个物品,那么dp[i][j]|=dp[i-1][j^v[i]]<<w[i],如果不选第i个物品,那么dp[i][j]|=dp[i-1][j],用bitset来执行集合相关的操作,比较经典,最后用滚动数组优化一下空间即可

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=1<<10;
const int inf=1e9+7;
const int mod=1e9+7;
int v[maxn],w[maxn];
bitset<maxn>dp[2][maxn];
void solve(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>v[i];
	}
	for(int i=0;i<2;i++){
		for(int j=0;j<maxn;j++){
			dp[i][j]=0;
		}
	}
	dp[0][0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<maxn;j++){
			//如果用w[i],v[i]
			dp[i&1][j]|=(dp[(i-1)&1][j^v[i]]<<w[i]);
			dp[i&1][j]|=dp[(i-1)&1][j];
		}
	}
	int ans=-1;
	for(int j=0;j<maxn;j++){
		if(dp[n&1][j][m]){
			ans=j;
		}
	}
	cout<<ans<<"\n";
}
signed main(){
	int t=1;
	cin>>t;
	while(t--){
		solve();
	}
}

第三场 Boss Rush

描述:
一个Boss有m滴血,现在有n个技能,每个技能有一个冷却时间t[i],表示t[i]内不能再使用任何技能,一个造成伤害时间len[i],表示在接下来的 j ∈ [ 0 , l e n [ i ] − 1 ] j\in[0,len[i]-1] j[0,len[i]1]内,每秒会造成d[i][j]的伤害,所有技能最多只能使用一次,现在问:能否打败Boss?,如果能打败Boss,需要的最短时间是多少?
思路:
首先我们可以确定最后释放技能的时候肯定是上一个技能的冷却时间一过就立马释放下一个技能,然后这题技能的个数很少,n<=18,所以我们可以往状态压缩那里想,我们考虑一下这个问题,如果给定你时间x,你在x时间内能打出的最高伤害是多少?,如果我们能确定最佳的使用技能顺序的话,那用最高伤害就很好算了,这样的时间复杂度是O(n^2)的,但是我们确定最佳技能顺序如果用枚举的方式的话是绝对会t的,我们要用上n比较小的条件的话,就可以往状压dp那里想,定义dp[state]为用了在state状态下的技能造成伤害的最大值,那么枚举一下到达state状态的最后一次用的技能j,则dp[state]由 d p [ s t a t e − ( 1 < < ( j − 1 ) ] + d a m a g e ( j ) dp[state-(1<<(j-1)]+damage(j) dp[state(1<<(j1)]+damage(j)转移过来,然后j技能之前释放了 s t a t e − ( 1 < < ( j − 1 ) state-(1<<(j-1) state(1<<(j1)状态的技能,所以j技能的造成伤害时间也就确定了,转移的时候取最大值即可,最后对于时间x二分一下,就可以得到需要的最短时间了

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=19;
const int inf=20;
const int mod=1e9+7;
int d[maxn][100005];
int t[maxn],len[maxn];
int state;
int pre[1<<maxn];
int n,m;
int dp[1<<maxn];
bool check(int x){
	//在x时间里面是否能够打败Boss
	int maxx=0;
	for(int i=1;i<=state-1;i++){
		dp[i]=0;
	}
	for(int i=1;i<=state-1;i++){
		for(int j=0;j<=n-1;j++){
			if((i>>j)&1){
				int tmp=i-(1<<j);
				int shenxia=x-pre[tmp];
				if(shenxia<0){
					continue;
				}
				int nowval;
				if(shenxia>=len[j+1]-1){
					nowval=d[j+1][len[j+1]-1];
				}
				else{
					nowval=d[j+1][shenxia];
				}
				dp[i]=max(dp[i],dp[tmp]+nowval);
				maxx=max(maxx,dp[i]);
			}
		}
	}
	return maxx>=m;
}
void solve(){
	cin>>n>>m;
	state=1<<n;
	int sum=0;
	for(int i=1;i<=n;i++){
		cin>>t[i]>>len[i];
		sum+=t[i];
		sum+=len[i];
		for(int j=0;j<=len[i]-1;j++){
			cin>>d[i][j];
			if(j)d[i][j]+=d[i][j-1];
		}
	}	
	for(int i=1;i<=state-1;i++){
		pre[i]=0;
		for(int j=0;j<=17;j++){
			if((i>>j)&1){
				pre[i]+=t[j+1];
			}
		}
	}
	int l=0,r=sum;
	int flag=0;
	while(l<r){
		int m=(l+r)>>1;
		if(check(m)){
			flag=1;
			r=m;
		}
		else{
			l=m+1;
		}
	}
	if(flag){
		cout<<l<<"\n";
	}
	else{
		cout<<"-1"<<"\n";
	}
}
signed main(){
	io;
	int t=1;
	cin>>t;
	while(t--){
		solve();
	}
}

第四场 Link with Bracket Sequence II

描述:
给定长度为n的括号序列,有m种括号,该括号序列中有些位置为空。问利用这m种括号填充括号序列中的空位置,将该序列变成合法括号序列的方案数。
思路:
区间dp,定义 d p i , j dp_{i, j} dpi,j 为区间 [ i , j ] [i, j] [i,j] 变为合法的括号序列方案数, 则它由各个子区间的 a [ k ] 和 a [ j ] 配对的方案个数 ∗ d p [ k + 1 ] [ j − 1 ] a[k]和a[j]配对的方案个数 * dp[k+1][j-1] a[k]a[j]配对的方案个数dp[k+1][j1]* d p [ i ] [ k − 1 ] dp[i][k-1] dp[i][k1]

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=500+5;
const int inf=1e9+7;
const int mod=1e9+7;
int a[maxn];
int dp[maxn][maxn];
void solve(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	if(n%2==1){
		cout<<"0"<<"\n";
		return;
	}
	else{
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				dp[i][j]=0;
			}
		}	
		for(int len=2;len<=n;len+=2){
			for(int i=1;i+len-1<=n;i++){
				int l=i;
				int r=i+len-1;  //l-r
				if(len==2){
					if(a[l]==0&&a[r]==0){
						dp[l][r]=m;
					}
					else if(a[l]+a[r]==0&&a[r]<0){
						dp[l][r]=1;
					}
					else if(a[l]+a[r]==0&&a[l]>0){
						dp[l][r]=1;
					}
					else if(a[l]==0&&a[r]<0){
						dp[l][r]=1;
					}
					else if(a[r]==0&&a[l]>0){
						dp[l][r]=1;
					}
				}
				else{
					for(int k=i;k<=r-1;k+=2){
						//l-k-1, k-r
						int res=0;
						int nowl=a[k],nowr=a[r];
						if(nowl==0&&nowr==0){
							res=m;
						}
						else if(nowl+nowr==0&&nowr<0){
							res=1;
						}
						else if(nowl+nowr==0&&nowl>0){
							res=1;
						}
						else if(nowl==0&&nowr<0){
							res=1;
						}
						else if(nowr==0&&nowl>0){
							res=1;
						}
						int tmp1=0;
						if(l>k-1){
							tmp1=1;
						}
						else{
							tmp1=dp[l][k-1];
						}
						int tmp2=0;
						if(k+1>r-1){
							tmp2=1;
						}
						else{
							tmp2=dp[k+1][r-1];
						}
						dp[l][r]+=res*tmp1%mod*tmp2%mod;
						dp[l][r]%=mod;
					}
				}
			}
		}
		cout<<dp[1][n]<<"\n";
	}
}
signed main(){
	//io;
	int t=1;
	cin>>t;
	while(t--){
		solve();
	}
}

第五场 BBQ

题意:
给定一个字符串,要求把字符串的每四个连续字符串变成aabb的形式,每次操作可以修改删除或者添加一个字符,问需要的最小操作
思路:
需要注意到一个性质,原串中的某个区间,如果其长度大于等8,就一定没必要变成新串的一个组。
因为,假设其长度为 n n n n > = 8 n>=8 n>=8,变成一个组需要其长度变为 4 4 4,至少需要先删掉 n − 4 n-4 n4个字符,总共需要不低于 n − 4 n-4 n4的代价。而如下策略一定可以不低于上述代价完成变换:删掉后面的 n − 8 n-8 n8个字符,然后对于剩下的8个字符,前 4 4 4个字符和后 4 4 4个字符分别花费2的代价进行修改,一定可以得到两个合法的组 ,这样总共是不高于 n − 4 n-4 n4的代价。
因此转移时可以只枚举前面长度不超过7的区间来转移。
d p [ n ] = min ⁡ i = 1 7 ( d p [ n − i ] + d p[n]=\min _{i=1}^{7}(d p[n-i]+ dp[n]=mini=17(dp[ni]+ 代 价 ( n − i + 1 , n ) ) (n-i+1, n)) (ni+1,n)),或者直接删除这个字符串 d p [ n ] = min ⁡ ( d p [ n ] , d p [ n − 1 ] + 1 ) d p[n]=\min (dp[n],dp[n-1]+1) dp[n]=min(dp[n],dp[n1]+1),考虑如何计算 ( n − i + 1 , n ) (n-i+1,n) (ni+1,n)变成aabb形式的代价,可以把一段区间中的字符的相等的关系记作一种模式,例如 “abcd"可以记作"0123”,“defg"也可以记作"0123”,“abccba"和"cbaabc"都可以记作"012210”,即相同的字符用相同的数字替换,不同的字符用不同的数字替换,并且按该字符最左出现位置从0开始编号,可以发下这样的模式其实是很少的,分别对每种模式提前算好代价就可以了。各个模式修改成aabb形式的最小代价是经典的通过删除添加替换把字符串A变成B需要的最小操作数的dp。

#include<bits/stdc++.h>
#define U unsigned
#define LL long long
#define UL U LL
using namespace std;
int t[10];
int g[10][5];
char w[3000000];
int cnt=0;
void dfs(int n,int c,int idx)
{
	cnt++;
	int m=9999999;
	if(n){
		for(int a=1;a<=7;a++)
			for(int b=1;b<=7;b++)
		{
			int p[5]={0,a,b,b,a};
			memset(g,1,sizeof g);
			for(int i=0;i<=4;i++)
				g[0][i]=i;
			for(int i=0;i<=7;i++)
				g[i][0]=i;
			for(int i=1;i<=n;i++)
				for(int j=1;j<=4;j++)
					g[i][j]=std::min(std::min(g[i-1][j]+1,g[i-1][j-1]+(t[i]!=p[j])),g[i][j-1]+1);
			if(g[n][4]<m)m=g[n][4];
		}
	}
	if(n)w[idx]=m;
	if(n==7)return;
	n++;
	for(int i=1;i<=c;i++)
	{
		t[n]=i;
		dfs(n,c,idx*8+i);
	}
	t[n]=c+1;
	dfs(n,c+1,idx*8+c+1);
}
string s;
int dp[1000001];
int last[26];
int pre[1000001];
void solve()
{
	cin>>s;
	int n=s.size();
	s=" "+s;
	memset(dp+1,10,n*4);
	memset(last,-1,sizeof last);
	
	for(int i=1;i<=n;i++)
	{
		pre[i]=last[s[i]-'a'];
		last[s[i]-'a']=i;
	}
	
	for(int i=0;i<n;i++)
	{
		dp[i+1]=min(dp[i+1],dp[i]+1);
		int cnt=0;
		int idx=0;
		int tmp[8];
		for(int j=1;j<=7&&i+j<=n;j++)
		{
			int c=s[i+j]-'a';
			if(pre[i+j]<=i)
				idx=idx*8+ (tmp[j]=++cnt);
			else
				idx=idx*8+ (tmp[j]=tmp[pre[i+j]-i]);
			dp[i+j]=min(dp[i+j],dp[i]+w[idx]);
		}
	}
	cout<<dp[n]<<"\n";
}
signed main(){
	dfs(0,0,0);
	int t;
	cin>>t;
	while(t--){
		solve();
	}
}

第七场 Sumire

题意: 给定 l , r , k , B , d l, r, k, B, d l,r,k,B,d ,计算 ∑ i = l r f k ( i , B , d ) \sum_{i=l}^{r} f^{k}(i, B, d) i=lrfk(i,B,d) ,其中 f ( i , B , d ) f(i, B, d) f(i,B,d) 表示数字 i i i B B B 进制下数码 d d d 出现次数,不含前导零。 T T T 组测试数 据, 1 ≤ l ≤ r ≤ 1 × 1 0 18 , 0 ≤ d < B ≤ 1 × 1 0 9 , 0 ≤ k ≤ 1 0 9 1 \leq l \leq r \leq 1 \times 10^{18} , 0 \leq d<B \leq 1 \times 10^{9} , 0 \leq k \leq 10^{9} 1lr1×10180d<B1×1090k109 ,定义 0 0 = 0 0^{0}=0 00=0
思路:
经典的数位dp,定义一个node结构体,结构体里面分别用vis记录是否搜索过这个情况,times[i]记录有多少数字在在 B B B 进制下数码 d d d 出现次数为 i i i,观察到 B B B很大,直接枚举当前pos的位是多少肯定会t,但是实际上当前位取1~B-1的区别就是是否等于d,然后还有对前导0以及limit(是否顶着上界)的影响,然后对于当前位取前导0和limit分别判断,再对于当前位取1-upper-1就可以统一处理了,最后 ( l , r ) (l,r) (l,r)区间内 d d d出现 i i i次的数的个数为 t i m e s [ i ] times[i] times[i],则贡献为 i k ∗ t i m e s [ i ] i^{k}*times[i] iktimes[i],题目本身不难,但是写起来比较麻烦,需要注意很多细节

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=105;
const int inf=1e9+7;
const int mod=1e9+7;
int digit[maxn];
int cnt;
int k,b,d,l,r;
struct node{
    int times[100]={0};  //times[i] 代表各位上d出现i次的有多少个
    int val=0;
    int vis=0;
};
node dp[maxn][2];
void init(){
    for(int i=0;i<=80;i++){
        for(int j=0;j<=1;j++){
            memset(dp[i][j].times,0,sizeof(dp[i][j].times));
            dp[i][j].vis=0;
            dp[i][j].val=0;
        }
    }
}
int ksm(int x,int n){
    if(x==0&&n==0){
        return 0;
    }
    int ans=1;
    while(n){
        if(n&1){
            ans=ans*x%mod;
        }
        x=x*x%mod;
        n>>=1;
    }
    return ans;
}
void cal(int x){
    cnt=0;
    while(x){
        int tmp=x%b;
        digit[++cnt]=tmp;
        x/=b;
    }
}
node dfs(int pos,int flag,int check){  //check用来表示是否出现过非0   
    if(pos==0){
        node tmp;
        tmp.vis=1;
        tmp.times[0]=1;  //d作为位数出现1次的数有1个了
        return tmp;
    }
    if(!flag&&dp[pos][check].vis!=0){
        return dp[pos][check];
    }
    int upper=flag?digit[pos]:b-1;
    node tmp;
    tmp.vis=1;
    node res;
    //对于i==0和i==upper对dfs系数有影响,所以拿出来分别判断一下
    for(int i=0;i<=upper;i+=max(upper,1ll)){
        //看一下各自有多少个
        res=dfs(pos-1,flag&&i==upper,check||i);
        for(int j=0;j<=80;j++){
            int shu=res.times[j];
            //存在j个位为d的数有shu个
            if(check){  //出现过非0,直接判断是否i等于d
                if(i==d){
                    tmp.times[j+1]+=shu;
                    tmp.times[j+1]%=mod;
                }
                else{
                    tmp.times[j]+=shu;
                    tmp.times[j]%=mod;
                }
            }
            else{  //都是前导0
                if(d==0){  //如果当前数还是0,还是没有贡献
                    tmp.times[j]+=shu;
                    tmp.times[j]%=mod;
                }
                else{
                    if(i==d){
                        tmp.times[j+1]+=shu;
                        tmp.times[j+1]%=mod;
                    }
                    else{
                        tmp.times[j]+=shu;
                        tmp.times[j]%=mod;
                    }
                }
            }
        }
    }
    //对于i==1~upper-1可以统一处理
    int cntt=upper-1;
    if(cntt>0){
        res=dfs(pos-1,0,1);
        if(d>=1&&d<=upper-1){
            //说明有1次是i==d cntt-1次 i!=d
            
            //1次 i==d
            int i=d;
            for(int j=0;j<=80;j++){
                int shu=res.times[j];
                if(check){
                    if(i==d){
                        tmp.times[j+1]+=shu;
                        tmp.times[j+1]%=mod;
                    }
                    else{
                        tmp.times[j]+=shu;
                        tmp.times[j]%=mod;
                    }
                }
                else{
                    if(d==0){
                        tmp.times[j]+=shu;
                        tmp.times[j]%=mod;
                    }
                    else{
                        if(i==d){
                            tmp.times[j+1]+=shu;
                            tmp.times[j+1]%=mod;
                        }
                        else{
                            tmp.times[j]+=shu;
                            tmp.times[j]%=mod;
                        }
                    }
                }
            }
            //cntt-1次 i!=d
            if(cntt-1>0){
                i=-1;
                for(int j=0;j<=80;j++){
                    int shu=res.times[j];
                    //i出现了shu次
                    if(check){
                        if(i==d){
                            tmp.times[j+1]+=shu*(cntt-1);
                            tmp.times[j+1]%=mod;
                        }
                        else{
                            tmp.times[j]+=shu*(cntt-1)%mod;
                            tmp.times[j]%=mod;
                        }
                    }
                    else{
                        if(d==0){
                            tmp.times[j]+=shu*(cntt-1)%mod;
                            tmp.times[j]%=mod;
                        }
                        else{
                            if(i==d){
                                tmp.times[j+1]+=shu*(cntt-1);
                                tmp.times[j+1]%=mod;
                            }
                            else{
                                tmp.times[j]+=shu*(cntt-1);
                                tmp.times[j]%=mod;
                            }
                        }
                    }
                }
            }
        }
        else{
            //cntt次 i!=d
            int i=-1;
            for(int j=0;j<=80;j++){
                int shu=res.times[j];
                //i出现了shu次
                if(check){
                    if(i==d){
                        tmp.times[j+1]+=shu*(cntt);
                        tmp.times[j+1]%=mod;
                    }
                    else{
                        tmp.times[j]+=shu*(cntt);
                        tmp.times[j]%=mod;
                    }
                }
                else{
                    if(d==0){
                        tmp.times[j]+=shu*(cntt);
                        tmp.times[j]%=mod;
                    }
                    else{
                        if(i==d){
                            tmp.times[j+1]+=shu*(cntt);
                            tmp.times[j+1]%=mod;
                        }
                        else{
                            tmp.times[j]+=shu*(cntt);
                            tmp.times[j]%=mod;
                        }
                    }
                }
            }
        }
    }
    if(!flag){
        dp[pos][check]=tmp;
    }
    if(pos==cnt){
        int anss=0;
        for(int i=0;i<=80;i++){
            anss=anss+ksm(i,k)*tmp.times[i]%mod;
            anss%=mod;
        }
        tmp.val=anss;
    }
    return tmp;
}
void solve(){
    //x以b进制表示下d出现次数的k次方
    cin>>k>>b>>d>>l>>r;
    cal(r);
    init();
    int ans2=dfs(cnt,1,0).val;
    init();
    cal(l-1);
    int ans1=dfs(cnt,1,0).val;
    cout<<(ans2-ans1+mod)%mod<<"\n";
}
signed main(){
    int t=1;
    cin>>t;
    while(t--){
        solve();
    }
}

第九场 Matryoshka Doll

题意:
给定n个数,要求把n个数分成k组,给定r,每组数假设长度为 m m m,要求 ∀ 1 ≤ i < m , a i + r ≤ a i + 1 \forall 1 \leq i<m, a_{{i}}+r \leq a_{{i+1}} ∀1i<m,ai+rai+1
思路:
先从小到大排序,定义dp[i][j]表示前面i个数分为j组有多少种划分,对于当前这个数如果把它划分到新的一组那么dp[i][j]+=dp[i-1][j-1],如果把这个数x划分到前面的j组之一当中,显然有一些组的最后一个数y不满足y+r<=x,至于有多少组不满足呢?答案就是前面不满足y+r<=x的数的数量,假设这些数的集合为S,显然这些数彼此之间都不能相邻,所以就是他们组的最后一个数,所以不满足的组数就是这些数的数量,所以最后dp[i][j]+=dp[i-1][j]*(j-不满足的数量)

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=5e3+5;
const int mod=998244353;
int w[maxn];
int dp[maxn][maxn];
int pre[maxn];
void solve(){
	int n,k,r;
	cin>>n>>k>>r;
	for(int i=1;i<=n;i++){
		cin>>w[i];
	}
	sort(w+1,w+1+n);
	for(int i=1;i<=n;i++){
		pre[i]=pre[i-1]+(w[i]!=w[i-1]);
	}
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=k;j++){
			dp[i][j]=dp[i-1][j-1];  //直接新放一个位置
			int pos=upper_bound(w+1,w+1+n,w[i]-r)-w;
			pos--;			
			if(pos<=i-1){
				int shenxia=i-pos-1;
				dp[i][j]+=dp[i-1][j]*(j-shenxia)%mod;
				dp[i][j]%=mod;
			}
		}
	}
	cout<<dp[n][k]<<"\n";
}
signed main(){
	int t=1;
	cin>>t;
	while(t--){
		solve();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值