The 2022 ICPC Asia Regionals Online Contest (I)

题目链接:PTA | 程序设计类实验辅助教学平台

D.Find the Number

题目:

样例输入: 

5
38 47
57 86
23 24
72 83
32 33

样例输出:

-1
68
-1
-1
-1

题意:定义一个好数:二进制表示中1的个数和后缀0的个数相同的数。

每次询问给出一组[l,r],输出该区间内的任意一个好数,如果没有就输出-1.

这道题我用两种方法来进行分析:

我的第一种思路就是枚举后缀0的个数,然后采取贪心构造的方法,让r逐渐减少来进行构造。我们从r的高位为1的地方开始找,找到第一个与l二进制中不同的数位,然后就可以从这一位开始进行判断,但是在找不同的数位的过程中由于相同数位的值已经被确定,所以我们需要统计过程中已经确定的1的个数,假如要枚举后缀0个数为5的情况,那么我们就知道倒数第六位一定是1,那么我们的目标就是在除了前六位的其他数位上放置4个1,我们先去构造那些我们不能抉择的数位,也就是从高位开始r和l数位上的数值相同的数位,因为我们要构造的数是介于l和r之间的,所以这些数位本质上是已经确定的了,假如我们已经找到了第id位l和r上二进制位是不同的,在找到这一位之前我们已经使用了一个1,那么我们就需要在id~7之间放置3个1,这个时候我们贪心来放置,假如我们先在id位放置一个1,这个时候我们构造的数还是有可能大于r的,但是一定是大于l的,这个我们只需要最后判断一下即可,然后为了使得我们构造的数尽可能小,所以我们选择在低数位8~7这两个位置放置两个1,然后我们比较一下我们当前构造的数和r的关系即可,如果当前这个数是小于等于r的,那么这个数就是符合题意的一个数,直接输出即可。如果大于r,那么也就是说我们不可能在第id位放置1,这个时候我们尝试在第id-1位放置1,这个时候我们构造的数一定是小于r的,为了尽可能使得我们构造的数是大于等于l的,我们尝试直接在高三位id-1~id-3上放置三个1,这是我们能构造的最大的满足题意的数,如果是大于等于l,那么这也是一个符合题意的解,如果上述两种情况都不能构造成功,则一定无解。

细节见代码:
 

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
int main()
{
	int T;
	cin>>T;
	while(T--)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		int ans=-1;
		for(int k=1;k<=16;k++)
		{
			int tans=1<<k;
			int id=k,cnt=k-1;
			for(int i=30;i>k;i--)//找到l和r中二进制不同的最高位 
			{
				if((l>>i&1)==(r>>i&1))
				{
					if(l>>i&1) tans+=1<<i,cnt--;
				}
				else
				{
					id=i;
					break;
				}
			}
			if(cnt<=0)
			{
				if(cnt==0&&tans>=l&&tans<=r)
				{
					ans=tans;
					break;
				} 
				continue;
			}
			if(id-k<cnt) continue;//剩余所有位均填1也不可能满足题意 
			int res=tans,tt=cnt;
			//先尝试在第id位放置1,然后从第i+1位往高位放置1 
			tans+=1<<id;cnt--;
			for(int i=k+1;cnt>0;cnt--,i++)
			{
				tans+=1<<i;
			} 
			if(tans<=r&&tans>=l)
			{
				ans=tans;
				break;
			}
			//从第id-1位往低位放置1 
			tans=res;cnt=tt;
			if(id-1-k<cnt) continue;
			for(int i=id-1;cnt>0;cnt--,i--)
				tans+=1<<i;
			if(tans<=r&&tans>=l)
			{
				ans=tans;
				break;
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

下面说一下第二种思路:

一开始这道题我是想着打表去做,然后写了个暴力发现1~1e9之间的好数大概有5e5个,显然是放不下的,所以就放弃了这个思路。但是我们可以直接在运行时爆搜出所有的好数并存储在vector中,然后对于每组询问我们直接二分答案即可

细节见代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
const int N=20;
vector<int>p[N];//p[i]里面存储i个后缀0的好数 
void dfs(int t,int now,int pos,int cnt)//表示我们当前的数是now,枚举到了第pos位,还差cnt个1就构成了t个后缀0的好数 
{
	if(pos>30||cnt<0) return ;
	if(cnt==0) 
	{
		p[t].push_back(now);
		return ; 
	}
	dfs(t,now,pos+1,cnt);
	dfs(t,now+(1<<pos),pos+1,cnt-1);
}
int main()
{
	for(int i=1;i<=16;i++)
	{
		dfs(i,1<<i,i+1,i-1);
		sort(p[i].begin(),p[i].end());
	}
	int T;
	cin>>T;
	while(T--)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		for(int i=1;i<=15;i++)
		{
			int ll=0,rr=p[i].size()-1;
			while(ll<rr)
			{
				int mid=ll+rr>>1;
				if(p[i][mid]>=l) rr=mid;
				else ll=mid+1;
			}
			if(p[i][ll]<=r&&p[i][ll]>=l)
			{
				printf("%d\n",p[i][ll]);
				break;
			}
			else if(i==15)
				puts("-1");
		}
	}
	return 0;
}

A 01 Sequence

 样例输入:

7 7
1000011
1 3
2 4
3 5
4 6
5 7
1 6
2 7

样例输出:

0
1
1
0
0
1
1

题意:给定一个长度为n的01串,每次询问给定一个区间[l,r](区间长度是3的倍数),我们把该区间内的字符串取出来首尾相接形成环,然后对于每个1,我们可以将其和其相邻的两个字符一块消除,我们可以进行一种操作,就是每次操作就是选定一个0将其变成1,问我们至少需要操作多少次才能将该环内的字符全部消除。

分析:首先我们能够发现,最少的操作次数也就是等价于在不进行操作的前提下用1来尽可能地消除0,最后剩余的0的最小数目除以3就是我们的最少操作次数。现在我们就转化为求解如何利用已有的1来尽可能多地消除0.通过简单的模拟一些小的数据可以发现,如果两个1连在一起,那么他们只能整体进行消除,也就是说每个1的贡献都是0.5(我们这里的贡献就是消除0的个数),而单独的一个1就可以消除两个0,也就是贡献是2,由此我们就可以得出一个结论,连续的k个1的贡献和k的奇偶性有关,如果k是偶数,那么贡献就是k/,否则就是(k-1)/2+2,这样我们就可以求出来连续1的贡献了,这时候大家可能会产生这样一个疑问,就是说比如我一个奇数个1和奇数个1之间原来有0相隔,但是把0消掉之后连到一起反而使得1的贡献降低,怎么避免这种情况呢?其实大家可以这么想,在一开始我们把连续的0都尽可能地消至1个0,那么这个时候我们就是用单独的0把一些连续的1间隔了,那么这个时候我们一定可以把0全部消除,对应的策略就是我们的连续的1先用来消除0多的那一块,最后使得两端的0都为1个即可。分析到这我们就知道了怎么知道一个环能不能消除完所有的0,但是我们不可能对于每一次询问都遍历一遍求取连续的1的贡献,所以我们可以预处理出连续的1的贡献,我们能够发现对于一个询问只有询问两端的1可能连起来,而中间部分的1的贡献我们都可以使用前缀和预处理,所以对于每次询问我们只需要统计一下两端的1连起来的贡献即可。这个我们可以提前预处理一下0的位置,这样就可以O(1)统计贡献了。

细节见代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
const int N=1e6+10;
char s[N];
int pre[N],last[N]; 
int f[N],sum[N];
//pre[i]记录第i个数后面第一个0的位置
//last[i]记录第i个数前面第一个0的位置
//sum[i]记录第i个数及之前0的个数 
//f[i]记录第i个数及之前1的贡献(第i个位置为0时有效) 
int main()
{
	int n,m;
	scanf("%d%d",&n,&m); 
	scanf("%s",s+1);
	for(int i=1;i<=n;i++)
		if(s[i]=='0') pre[i]=i,sum[i]=sum[i-1]+1;
		else pre[i]=pre[i-1],sum[i]=sum[i-1];
	last[n+1]=n+1;
	for(int i=n;i>=1;i--)
		if(s[i]=='0') last[i]=i;
		else last[i]=last[i+1];
	int id=last[1];
	while(id<=n)
	{
		int len=id-pre[id-1]-1;
		int res;//记录当前段连续1的贡献 
		if(len%2==0) res=len/2;
		else res=(len-1)/2+2; 
		f[id]=f[pre[id-1]]+res;
		id=last[id+1];
	}
	while(m--)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		int len=(last[l]-l)+(r-pre[r]);//统计一下跨越两端的连续1的数量 
		int res;//记录该环的1的总贡献
		if(len%2==0) res=len/2;
		else res=(len-1)/2+2;
		res+=f[pre[r]]-f[last[l]];
		if(res>=(sum[r]-sum[l-1])) puts("0");
		else printf("%d\n",(sum[r]-sum[l-1]-res)/3);
	} 
	return 0; 
} 

L LCS-like Problem

 样例输入:

apple
grape

样例输出:

3

题意:给定一个s串和t串,问s串中满足其子序列与t串的最长公共子序列长度小于等于1的最长子序列的长度。

分析:首先我们先来看一下t串的作用是什么,它能够告诉我们哪些字符是不能前后存在的,拿样例来说,t串为grape,首先ge这个序列是不能存在的,有位ge这个序列和t串的最长公共子序列长度是大于1的,所以我们就可以预处理出来哪两个字符是不能先后存在的,为什么是两个呢?因为多个字符是以两个字符为基础的,两个字符都不存在更不可能存在多个字符。这一部分复杂度是O(26*n),然后我们设f[i][j]表示匹配到s串的第i个字符以字符j结尾的满足题意的序列的最大长度,接下来我们进行状态转移方程的推导:

我们来讨论一下s串的第i个字符,如果s串的第i个字符没有在t串中存在,那么对于所有的0<=j<26我们显然有f[i][j]=f[i-1][j]+1,这个地方需要注意一下,就是我们状态明明设置的以字符j结尾的情况的最大长度,为什么添加一个t中不存在的字符后他会使得所有满足题意的序列长度+1?我们可以这样理解,既然当前字符在t中没有出现过,那么我放在哪根本不会产生影响,我们可以先考虑将其放在了字符j的前面(当然这样理解是有违背子序列的含义的,一会我会说这样做的原因),下面来看看s串的第i个字符在t串中出现过的情况,那么我们就枚举可以放在当前字符前面的所有字符,然后令f[i][s[i]]=max(f[i][s[i]],f[i][j]+1)即可,也就是需要满足vis[j][s[i]]=true的条件。需要注意的一点是我们对于更新i时一定要继承i-1的所有方案,这个是大部分动态规划都需要做的事情,这里就不赘述了。这个时候可能会产生一个疑问:就是我们放置当前字符s[i]时只考虑了他前面一个字符,但是并没有考虑在前一个字符之前的字符啊,比如是abc万一要是出现a和b不矛盾,b和c不矛盾但是a和c矛盾怎么办?其实这个是不可能出现的,假设a,b,c在s串和t串中均出现过,否则可以直接放不需要讨论,首先我们看一下a和b不矛盾是属于什么情况,说明在t串中b是出现在字符a之前的,而b和c不矛盾则说明c是出现在字符b之前的,那么我们就能得到字符a是出现在c之前的,所以不可能会有矛盾。这也就是我们结尾的字符的含义是特指s串和t串公共字符的原因,否则这里会出现错误,简单模拟一下就知道错误在哪了。

细节见代码:
 

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
const int N=5e5+10;
map<int,int>mp;
int f[N][30];
//f[i][j]表示匹配到s串的第i个字符以字符j结尾的满足题意的序列的最大长度 
bool vis[30][30];
//vis[i][j]=true/falss表示字符i和字符j可以/不可以出现先后出现 
char s[N],t[N];
int main()
{
	memset(vis,true,sizeof vis);
	scanf("%s",s+1);
	scanf("%s",t+1);
	int n=strlen(s+1);
	int m=strlen(t+1);
	for(int i=1;i<=m;i++)
	{
		for(int j=0;j<26;j++)
			if(mp[j])
				vis[j][t[i]-'a']=false;
		mp[t[i]-'a']=1;
	}
	for(int i=1;i<=n;i++)
		f[i][s[i]-'a']=1;
	for(int i=2;i<=n;i++)
	{
		int t=s[i]-'a';
		for(int j=0;j<26;j++)
			f[i][j]=f[i-1][j];
		if(!mp[t])//该字符没有在字符串t中出现过
		{
			for(int j=0;j<26;j++)//考虑加上当前字符,不过只需要记录长度 
				f[i][j]++;
		}
		else
		{
			for(int j=0;j<26;j++)//考虑加上当前字符
				if(vis[j][t])//表示字符j和字符t可以相邻 
					f[i][t]=max(f[i][t],f[i-1][j]+1);
		}
	}
	int ans=0;
	for(int i=0;i<26;i++)
		ans=max(ans,f[n][i]);
	printf("%d\n",ans);
	return 0;
} 

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值