Codeforces Round #744 (Div. 3) 解题报告

8 篇文章 0 订阅
3 篇文章 0 订阅

这套题打的时候只写了前5个题,都比较基础,但是后面三个题补了之后还是收获很大,记录一下题解报告和一些需要注意的细节

A. Casimir’s String Solitaire

题意:一个由A,B,C组成的字符串,每次可以同时删除A和B或者B和C,给一个字符串,问能否删干净。

考虑删除的一个不剩,每次都要消耗两个,只需要判断A的数量+C的数量是否等于B的数量即可

 
#include<bits/stdc++.h>
using namespace std;
int t,a,b,c;
string s;
int main()
{
	cin>>t;
	while(t--)
	{
		a=0,b=0,c=0;
		cin>>s;
		int len=s.size();
		for(int i=0;i<len;i++)
		{
			if(s[i]=='A')a++;
			if(s[i]=='B')b++;
			if(s[i]=='C')c++;
		}
		if(b==a+c)cout<<"YES"<<endl;
		else cout<<"NO"<<endl;
	}
	return 0;
}

B. Shifting Sort

给一个数组,和一个排序方式,就是循环向左移位,让你构造出一种满足题意的构造方案。

观察循环移位操作,不难发现,我们可以通过类似选择排序的思想来完成这个排序方案的构造,每次把一个数字放到它应该存在的位子上,n步以内一定能构造出结果,至于如何防止,观察循环移位,就能发现,其实就是吧选择范围内所有数后移一位,然后把最后的放到第一个去。模拟一些这个过程即可,具体可以参考代码实现

#include<bits/stdc++.h>
using namespace std;
long long int t,n,a[1010],b[1010],l[1010],r[1010],d[1010],cnt;
int main()
{
	cin>>t;
	while(t--)
	{
		cin>>n;
		for(int i=1;i<=n;i++)
		{
			cin>>a[i];
			b[i]=a[i];
		}
		sort(b+1,b+1+n);
		cnt=0;
		for(int i=1;i<n;i++)
		{
			int flag=0;
			for(int j=i;j<=n;j++)
			{
				if(a[j]==b[i])
				{
					flag=j;
					break;
				}
				
			}		
			if(i!=flag){
				l[cnt]=i;
				r[cnt]=flag;
				d[cnt]=flag-i;
				cnt++;
				for(int k=flag-1;k>=i;k--)
				a[k+1]=a[k];
				a[i]=b[i];   
			}
		}
		cout<<cnt<<endl;
		for(int i=0;i<cnt;i++)
		cout<<l[i]<<" "<<r[i]<<" "<<d[i]<<endl;
	}
	return 0;
}

C. Ticks

这道题在前5个里面算比较麻烦的了,不过其实就是一个模拟题,按照题目要求,判断是否可能存在合法的情况,其实我们只需要按顺序,把所有可以向上扩展的*的位置都向上枚举扩展,最后检查能否全部覆盖即可。

#include<bits/stdc++.h>
using namespace std;
int t,n,m,k;
char a[100][100];
int vis[100][100];
int check(int x,int y)
{
	int cnt=1;
	while(1)
	{
		if(a[x-cnt][y-cnt]=='*'&&a[x-cnt][y+cnt]=='*')cnt++;
		else break;
	}
	return cnt-1;
}
int main()
{
	cin>>t;
	while(t--)
	{
		cin>>n>>m>>k;
		memset(vis,0,sizeof(vis));
		for(int i=1;i<=n;i++)
		scanf("%s",a[i]+1);
		for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			if(a[i][j]=='*')
			{
				int w=check(i,j);
				if(w>=k)
				{
					for(int k=0;k<=w;k++)
					{
						vis[i-k][j-k]=1;
						vis[i-k][j+k]=1;	
					}
				}
			}
		}
		int flag=0;
		for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			if(a[i][j]=='*'&&vis[i][j]==0)
			 flag=1;
		}
		if(flag==1)cout<<"NO"<<endl;
		else cout<<"YES"<<endl;
	}
	return 0;
}

D. Productive Meeting

题意是,n个人,每个人有一个社交指数,代表它可以进行这么多次社交。我们要让每个人都尽可能多交流,使得最终交流次数最大。

其实就是个贪心的想法,每次只要保证当前社交指数最大的人参与社交即可,刚开始我想同时维护一段区间里最大和最小的值,感觉线段树勉强可以实现题目保证了ai的综合范围,nlog看上去还是能过的。但是不太好写。重新观察,发现我们只需要取出最大值就行了,另一个值是无关紧要的,至于为什么,可以用数学方法证明,也可以简单理解一下。因为我们始终保持最大的还在减少,不会出现2 2 2,这种情况,我们动态维护最大值,所以我们的问题就简化成了只需要维护下最大值就行了,为了方便,另一个值就取次大值即可。

#include<bits/stdc++.h>
using namespace std;
int t,n;
struct node{
	int val,id;
	bool operator <(const node &a)const
	{
		return val<a.val;
	}
}a[200010];
int l[200010],r[200010];
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		 priority_queue<node> p;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i].val);
			a[i].id=i;
			if(a[i].val>0)
			p.push(a[i]);
		}
		int cnt=0;
		while(p.size()>1)
		{
			cnt++;
			node tmp1=p.top();p.pop();
			node tmp2=p.top();p.pop();
			l[cnt]=tmp1.id;
			r[cnt]=tmp2.id;
			tmp1.val--;
			tmp2.val--;
			if(tmp1.val>0)p.push(tmp1);
			if(tmp2.val>0)p.push(tmp2);
		}
		printf("%d\n",cnt);
		for(int i=1;i<=cnt;i++)
		
			printf("%d %d\n",l[i],r[i]);
			
	}
	return 0;
}

E1. Permutation Minimization by Deque

通过率比B还高,是个纯水题。给你一个构造方法,和一串数字,问这么保证字典序最小,我们就按照字典序的定义,模拟每个数的过程就行,可以用deque,由于我不太熟悉这个stl,直接用数组实现了。

#include<bits/stdc++.h>
using namespace std;
int t,n;
int a[200010];
int ans[400010];
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
		}
		int l=199999,r=200001;
		int now=a[1];
		ans[200000]=a[1];
		for(int i=2;i<=n;i++)
		{
			if(a[i]<now)
			{
				ans[l]=a[i];
				l--;
				now=a[i];
			}
			else ans[r]=a[i],r++;
		}
		for(int i=0;i<=400001;i++)
		{
			if(ans[i]!=0){
				printf("%d ",ans[i]);
				ans[i]=0;
				}
		}
		printf("\n");
	}
	return 0;
}

E2. Array Optimization by Deque

构造方案的方式和E1是一样的,问题变成了让逆序对数量最小,其实是一个简单的贪心,简单思考,对于每一次新的值的插入,他都会在最前面或最后面,所以前面的所以选择对后续没有后效性。所以我们只需要局部贪心,每次插入都选择逆序对更少的一端即可。至于逆序对,离散化加树状数组维护即可。离散化是因为数组下标在这个过程中代表了数据的值,在依次插入数据的过程中,放到合适的位置,再用树状数组的性质计算前缀区间或后缀区间的最大值即可。

#include<bits/stdc++.h>
using namespace std;
int t,n,m,a[200010],b[200010],tr[200010];
int lowbit(int x)
{
	return x&-x;
}
void add(int x,int val)
{
	while(x<=m)
	{
		tr[x]+=val;
		x+=lowbit(x);
	}
}
long long cal(int x)
{
	long long tmp=0;
	while(x)
	{
		tmp+=tr[x];
		x-=lowbit(x);
	}
	return tmp;
}
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		memset(tr,0,sizeof(tr));
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			b[i]=a[i];
		}
		sort(b+1,b+1+n);
		m=unique(b+1,b+1+n)-b-1;
		for(int i=1;i<=n;i++)
		{
			a[i]=lower_bound(b+1,b+1+m,a[i])-b;
		}
		long long ans=0;
		for(int i=1;i<=n;i++)
		{
			ans+=min(cal(a[i]-1),cal(m)-cal(a[i]));
			add(a[i],1);
		}
		printf("%lld\n",ans);
	}	
	return 0;
}

F.Array Stabilization (AND version)

前几题刚做过这道题的gcd版本,是个二分加st表的应用,这道题换成了&操作,题意给定一个d,一个01序列,每次操作时把数组左移d位然后在于原数组取&操作。问需要第三次才能让数组全部为0。如果不行就输出-1

看到了很多图论相关的做法,无非就是从每个1点出去,维护一条环,看最长的环的长度时多少。可以用并查集维护。但是看了rainboy的代码,发现可以从数学角度解决这个问题,很有趣。解释一下这个做法的个细节,首先算出n和k的gcd值。这个值代表了回归原点过程中,每一步能走多少,也代表了又几种不同意义的出发点。还有需要注意的是,由于我们对小于每个点都当作起点进行枚举,然后跑两圈统计最长的连续1序列,经过这么多次运算。数组一定会情况。至于为什么跑两圈。关键点是,起点可能是第二个1,跑完一圈但是不能求出连续的1序列,必须跑两圈才能代表最长的1序0列。如果跑了两圈还没有断,说明这个环上全是1.没有0,那么无论如何旋转操作,都不会对最后的结果产生影响。

#include<bits/stdc++.h>
using namespace std;
int t,n,d,k,ans,a[1000010];
int gcd(int a,int b)
{
	return b==0?a:gcd(b,a%b);
}
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&k);
		d=gcd(n,k);
		for(int i=0;i<n;i++)
		scanf("%d",&a[i]);
		ans=0;
		
		for(int i=0;i<d;i++)
		{
			int cnt=0;
			for(int j=0;j<2*n/d;i=(i+k)%n,j++)//最坏情况需要跑两圈
			ans=max(ans,cnt=a[i]?cnt+1:0);//断了无所谓,跑两圈一定能找到最长的连续1序列
			if(cnt==2*n/d)
			{
				ans=-1;
				break;
			}
		}
		cout<<ans<<endl;
	}
}

G. Minimal Coverage

这道题也是一个很有意思的题目,大概意思是,一个青蛙,要跳n次,每次跳的值已知,可以左右跳,问至少一个多大的区间可以让他不越界。

首先观察性质,很容易发现,这个问题是单调的,如果小区间可以不会继续找大区间了,所以这部分可以二分完成,问题就转化为如何n次跳跃大小已知,问给定区间大小能否满足不出界。对于这个子问题,我们可以考虑进行一个可行性dp来解决。首先设置一个区间,长度固定,初始都为1,代表刚开始任何一个点都可以作为起点。由于这个问题中只需要01两个状态表示是否可行。我们可以用bitset来解决这个问题。每一次我们都把它进行区间左移x和区间右移x,然后取|这样会出现一些地方不能作为起点,他们的值就会标记为0,代表此位置在第i次跳跃后不可行了。然后在让他和初始状态取&,可以删除一些出界的状态。然后我们只需要判断整个范围内还有没有值为1的地方即可。这里可以用bitset内置的any来完成。刚开始觉得二分上界最多1000,wa了几次,后来发现在极端情况下。可能会有890, 900 这类数据的出现,只能从前面连续跳两下才能解决问题,所以上界应该取到2000.那么二分的复杂度就是log(2000)大概是10.然后dp复杂度是O(n)然后O(10n)按理说很快就能跑完了,并且看起来好像不用二分2000 *n也不超时。但是不知道为啥。不二分就直接wa掉了。初步怀疑bitset的<<操作不是O(1)的,如果谁知道具体原因,欢迎评论告知,我把二分的代码和暴力t掉的代码都放一下。

T了的

#include<bits/stdc++.h>
#include<bitset>
using namespace std;
int t,n;
int a[10010];
bitset<2010>f,b;
int check(int x)
{
	f.reset();
	for(int i=0;i<=x;i++)
	f[i]=1;
	b=f;
	for(int i=1;i<=n;i++)
	{
		f=(f<<a[i])|(f>>a[i]);
		f&=b;
	}
	return f.any();
}
int main()
{
	cin>>t;
	while(t--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
		for(int i=1;i<=2010;i++)
			if(check(i))
			{
				printf("%d\n",i);
				break;
			}
	}
	return 0;
}

二分的

#include<bits/stdc++.h>
#include<bitset>
using namespace std;
int t,n;
int a[10010];
bitset<2010>f,b;
int check(int x)
{
	f.reset();
	for(int i=0;i<=x;i++)
	f[i]=1;
	b=f;
	for(int i=1;i<=n;i++)
	{
		f=(f<<a[i])|(f>>a[i]);
		f&=b;
	}
	return f.any();
}
int main()
{
	cin>>t;
	while(t--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
		int l=1,r=2010,mid;
		while(l<r)
		{
			mid=(l+r)>>1;
			if(check(mid))
				r=mid;
			else l=mid+1;
		}
		printf("%d\n",l);
	}
	return 0;
}

除了上述的做法,在看别人代码时候,还学到了一种其他的dp方法,这里也一并介绍一下。这里的g数组和f数组意义就是第i次操作后到位置j至少需要多少格子。状态转移就是f[I] [j-a[i]]= min(f[i] [j-a[i]],f[i-1] [j]+k )代表了这一步从右到左需要多少额外扩展的才能过来。另一个同理。然后可以用滚动数组的技巧省一维。这样f数组始终是最新状态,g数组每轮初始化即可。状态转移一左一右。我感觉没有上面的二分好理解。而且复杂度也是O(2000* n)看上去和我上面T了的暴力一个复杂度。但是这个跑的很快。我也不太懂为啥。如果有知道的欢迎交流讨论。

#include <bits/stdc++.h>
using namespace std;
int i,j,k,n,m,t,res,f[3050],g[3050];
int main() {
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);
		memset(f,0x3f,sizeof(f));f[0]=0;
		while(n--){
			scanf("%d",&k);
			memset(g,0x3f,sizeof(g));
			for(j=0;j<=2000;j++){
				g[max(0,j-k)]=min(g[max(0,j-k)],f[j]+k);		
				g[j+k]=min(g[j+k],max(0,f[j]-k));
			}
			memcpy(f,g,sizeof(f));
		}
		res=114514;
		for(i=0;i<=2000;i++)res=min(res,i+f[i]);
		printf("%d\n",res);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值