2021ccpc女生赛 BCF题解

2021ccpc女生赛

C (状压DP)

题意:给定一张图,每个节点隶属于一个公司,会有节点属于相同的公司,节点都有一个权值,问从节点1出发,到达第 i i i个点时能得到的最大价值是多少。

每个公司的红包只能领取一次,假设节点3和5都属于公司2,则若在节点3处领取了红包,则在5处就不能领取了。

数据保证无重边和环。其中点数 2 ≤ n ≤ 36 2\leq n \leq 36 2n36

考虑状态压缩dp,

f [ i ] [ j ] 表示当前在点 i ,且当前各个公司的状态为 j , ( j 可看成 01 序列,这一位为 1 表示已经取过,为 0 表示未取过 ) ,能得到的最大价值 f[i][j]表示当前在点i,且当前各个公司的状态为j,(j可看成01序列,这一位为1表示已经取过,为0表示未取过),能得到的最大价值 f[i][j]表示当前在点i,且当前各个公司的状态为j,(j可看成01序列,这一位为1表示已经取过,为0表示未取过),能得到的最大价值

则状态转移为:

if(j&(1<<c[v])) f[v][j]=max(f[v][j],f[u][j-(1<<c[v])]); //取v
else f[v][j]=max(f[v][j],f[u][j]); //不取v

但是注意点的数量为36,因此这样做空间和时间都会超,因此考虑优化。

可以发现,若某个公司只有一家分店,那么我们可以直接取走,因为不会对后续的结果有影响。记为第一类公司。

只有有两家以上分店的公司才会对答案有影响。记为第二类公司。

因为总点数为36,因此第二类公司数量最多为 n / 2 = 18 n/2=18 n/2=18个,那么我们就可以进行转移了:

  1. 若当前终点为第一类公司,则直接取就行。
  2. 若当前终点为第二类公司,需要判断当前状态来进行转移。

为什么这样是对的呢,因为dp是无后效性的,本题也是如此,我们算出了当前节点的最优解,即可推出下一个节点的最优解,因此可以保证结果一定正确。

同时本题的图是一个天然的拓扑图,因此不用在拓扑序上dp,循环枚举起点即可。

code:

#include<bits/stdc++.h>

using namespace std;
#define endl '\n'
#define ios std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define x first
#define y second
typedef pair<int,int> pii;

//head
const int N=110;
vector<int> e[N];
int c[N],w[N];
int dp[50][1<<19];
vector<int> vv;
int ans[N];
void work()
{
	int n,m;
	cin>>n>>m;
	map<int, int> mp;
	for(int i=1;i<=n;i++){
		cin>>c[i];
		mp[c[i]]++;
	}
	for(int i=1;i<=n;i++) cin>>w[i];

	for(int i=1;i<=m;i++){
		int a,b;
		cin>>a>>b;
		e[a].push_back(b);
	}
	e[0].push_back(1);
	//这里也要连条边,不然点1无法转移
	for(auto [x,y]:mp){
		if(y>1) vv.push_back(x);//把所有第二类公司放入
	}
	int cnt=vv.size();
	for(int i=0;i<=n-1;i++){
		for(int k=0;k<(int)e[i].size();k++){//遍历所有邻点
			int v=e[i][k];
			int id=0;
			bool f=0;
			for(int j=0;j<cnt;j++){
				if(c[v]==vv[j]) {
					id=j; f=1;
					break;
				}
			}
			if(f){//当前终点为第二类公司
				for(int j=0;j<(1<<cnt);j++){
					if(j&(1<<id)) dp[v][j]=max(dp[v][j],dp[i][j-(1<<id)]+w[c[v]]);
					//若状态j是要取这家的红包,则只能由未取这家的状态转移过来
					else dp[v][j]=max(dp[v][j],dp[i][j]);
					ans[v]=max(ans[v],dp[v][j]);
				}
			}
			else {//当前终点为第二类公司
				for(int j=0;j<(1<<cnt);j++){
					dp[v][j]=max(dp[v][j],dp[i][j]+w[c[v]]);
					ans[v]=max(ans[v],dp[v][j]);
				}
		}	}
	}
	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;	
}
signed main()
{
	ios;
	int t;
	t=1;
	while(t--) work();

	return 0;
}

F (hash, 最小循环节)

题意:给定一个由字符组成的矩阵,询问q次,每次询问给出 x 1 , x 2 , y 1 , y 2 x1,x2,y1,y2 x1,x2,y1,y2,问这个矩阵最小是由哪个矩阵扩展来的,输出该矩阵的大小,即 长 ∗ 宽 长*宽 的值。

可以看出,行和列的答案是相互独立的,也就是其可以分开计算,那么对于行或列,我们要求的是什么呢?最短的循环的字符串,那和什么很像?对!最小循环节!我们可以用 k m p kmp kmp来求出这个最小循环节,那么问题又来了,对于 x 1 , y 1 , x 2 , y 2 x1,y1,x2,y2 x1,y1,x2,y2这个矩阵,对于列来说,我们求的其实是个二维的东西,列的每个元素其实是当前行的从 y 1 到 y 2 y1到y2 y1y2的这一段字符串。那么怎么把这一段的内容转化为一个值呢?

对于列来说,我们可以对每一行进行hash,利用hash值来表示这一段的内容是什么,也就把二维转化为了一维问题。

对于行来说操作也类似,对每一列hash即可。

code:

#include<bits/stdc++.h>

using namespace std;
#define endl '\n'
#define ios std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define x first
#define y second
typedef pair<int,int> pii;
typedef unsigned long long ull;
//head
const int N=2e3+10,P=1331,mod=1e9+7;

ull p[N];
ull r[N][N],c[N][N];
char g[N][N];
ull s[N];
int ne[N];
int n,q;
void init()
{
	p[0]=1;
	for(int i=1;i<=n;i++){
		p[i]=p[i-1]*P;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			r[i][j]=r[i][j-1]*P+g[i][j];
			c[j][i]=c[j][i-1]*P+g[i][j];
		}
	}
}
int get(int len)
{
	for(int i=2,j=0;i<=len;i++){
		while(j&&s[i]!=s[j+1]) j=ne[j];
		if(s[i]==s[j+1]) j++;
		ne[i]=j;
	}

	return len-ne[len];//返回最小循环节的长度
}
void work()
{

	cin>>n>>q;
	for(int i=1;i<=n;i++){
		cin>>g[i]+1;
		
	}
	init();
	
	while(q--)
	{
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		//分别处理行和列,找到最小循环节,相乘就是答案
		for(int i=x1;i<=x2;i++){//处理列
			s[i-x1+1]=r[i][y2]-r[i][y1-1]*p[y2-y1+1];
		}
		int len=x2-x1+1;
		int L=get(len);

		for(int i=y1;i<=y2;i++){//处理行
			s[i-y1+1]=c[i][x2]-c[i][x1-1]*p[x2-x1+1];
		}
		len=y2-y1+1;
		int R=get(len);
		cout<<L*R<<endl;
	}
}
signed main()
{
	ios;
	int t;
	t=1;
	while(t--) work();

	return 0;
}

B (倍增)

题意:给定一个长度为 n n n,且只由字母表前 m m m个字母组成的字符串 S S S,(不一定全部包含前m个字母),给出q次询问,每次询问给出 l , r l,r l,r,让你构造出一个长度为 k k k的字符串 T T T(也只能由前m个字母组成),使得 T T T不是 S l . . . S r S_l...S_r Sl...Sr的子序列,求出最小的k的值(不用真的构造出 T T T)。

1 ≤ n ≤ 2 ∗ 1 0 5 1\leq n \leq 2*10^5 1n2105 , 1 ≤ q ≤ 2 ∗ 1 0 5 1\leq q \leq 2*10^5 1q2105

注意到n和q都比较大,因此考虑怎么把结果预处理出来。怎么计算出结果呢?考虑怎么构造T使得k最小,对于T的某个字符来说,可以有m种选择,假设当前选择了 S i S_i Si ,那么下一步要选的位置为这m种字符中离 i i i最远的位置,若某种字符不存在,直接跳出选择。

我们可以把这种不断选择看成跳到某个位置,另 f [ i ] [ j ] 表示当前在位置 i ,跳(选择) 2 j 次可以到达的最远位置 f[i][j]表示当前在位置i,跳(选择)2^j次可以到达的最远位置 f[i][j]表示当前在位置i,跳(选择)2j次可以到达的最远位置

我们可以通过倍增来预处理出 f f f数组,同时注意 f f f数组的初始化。

code:

#include<bits/stdc++.h>

using namespace std;
#define endl '\n'
#define ios std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define x first
#define y second
typedef pair<int,int> pii;

//head
const int N=2e5+10;
int f[N][20];
int pos[50];
int m,n;
string s;
void init()
{
	vector<int> pos(26,n+1);
	//pos表示每个字符出现的位置,要实时更新
	//f[i][j]表示当前在第i位置,下一步有m种选择可以跳,跳2^j次后到达的最大位置

	for(int i=0;i<20;i++) f[n+1][i]=n+1;
	for(int i=n;i>=0;i--){
		for(int j=0;j<m;j++){
			f[i][0]=max(f[i][0],pos[j]);//取最远的那个字符
		}
		if(i) pos[s[i]-'a']=i;//更新pos,因为要算下一步跳的最远位置
	}
	for(int j=1;j<20;j++){
		for(int i=0;i<=n;i++){
			f[i][j]=f[f[i][j-1]][j-1];
		}
	}
}
void work()
{
	cin>>m>>n;
	cin>>s; s=" "+s;
	init();
	int q;
	cin>>q;
	while(q--)
	{
		int l,r;
		cin>>l>>r;
		int ans=0,p=l-1;
		for(int j=19;j>=0;j--){
			if(f[p][j]<=r){
				p=f[p][j];
				ans+=(1<<j);
			}
		}
		cout<<ans+1<<endl;
	}
}
signed main()
{
    ios;
	int t;
	t=1;
	while(t--) work();
	return 0;
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值