Codeforces Round 889 (Div. 2)补题

Dalton the Teacher(Problem - A - Codeforces

题目大意:现在有一个排列,我们需要对部分数调整顺序,使得每一位上的数和下标不同,问最少需要操作多少次。

思路:我们显然只用找到哪些位置上的数是对应的,如果有偶数个位置上是对应的,那么两两交换即可,如果是奇数个位置上对应,再随便多加一次交换即可。

#include<bits/stdc++.h>
using namespace std;
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n;
		scanf("%d",&n);
		int c=0;
		for(int i=1;i<=n;i++)
		{
			int x;
			scanf("%d",&x);
			if(x==i) c++;
		}
		int ans= ceil(c*1.0/2);
		printf("%d\n",ans);
	}
}

Longest Divisors Interval(Problem - B - Codeforces

题目大意:给定一个正整数n,我们要找到一段区间,使得区间中的每个数都可以被n整除。(n<=1e18)

思路:这题看上去没有任何头绪,我最初也是。但是留意到这个1e18,那么复杂度差不多就在logn,这样才不会超时,然后我试了好多构造,最后想了一种方法,每次n/=2,然后在这个数和它的左右判断是否是n的因数,如果是,以这点为中心,看看左右最多能够延伸多长。然后就水过了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
int check(int k)
{
	if(!k) return 0;
	if(n%k==0) return 1;
	else return 0;
}
int tj(int k)
{
	int c=1;
	int l=1,r=1;
	for(int i=1;k-i>0;i++)
	{
		if(l) 
		{
			if(n%(k-i)==0) c++;
			else l=0;
		}
		if(r)
		{
			if(n%(k+i)==0) c++;
			else r=0;
		}
		if(!l&&!r) break;
	}
	return c;
}
signed main()
{
	int t;
	scanf("%lld",&t);
	while(t--)
	{
		scanf("%lld",&n);
		if(n%2)
		{
			printf("1\n");
			continue;
		}
		int mid=n/2,ans=0;
		while(mid)
		{
			if(check(mid))
			{
				int c=tj(mid);
				ans=max(ans,c);
			}
			if(check(mid+1))
			{
				int c=tj(mid+1);
				ans=max(ans,c);
			}
			if(check(mid-1))
			{
				int c=tj(mid-1);
				ans=max(ans,c);
			}
			mid/=2;
		}
		cout<<ans<<endl;
	}
}

当然,既然是写补题的话,那么肯定不能这么水,我又研究了下官方题解,这里有个很重要的性质——如果一段区间的长度大于x,那么当中至少存在一个数是x的倍数。n是区间中的数的倍数,区间中的数是x的倍数,那么n是x的倍数,所以我们可以直接从1开始枚举长度x即可。

这样就很科学了,代码也很短

#include<bits/stdc++.h>
using namespace std;
#define int long long
signed main()
{
	int t;
	scanf("%lld",&t);
	while(t--)
	{
		int n;
		scanf("%lld",&n);
		int c=0;
		for(int i=1;i<=n;i++)
		{
			if(n%i==0) c++;
			else break;
		}
		printf("%lld\n",c);
	}
}

 Dual (Easy Version)(Problem - C1 - Codeforces

Dual (Hard Version)(Problem - C2 - Codeforces

题目大意:现在有一个长度为n的数组a[],我们可以进行k次操作,每次操作可以挑选两个数i,j,然后使ai+=aj,现在需要将数组变成非递减的,然后输出操作次数和具体的选择(i,j)。(1<=n<=20,-20<=a[i]<=20,k<=50)(hard版本k的限制是31,我们下面就按找hard版本的来讨论)

思路:这个题很容易想到的一点就是如果前一个数比后一个数大,后一个数加上前一个数操作一次就可以了,看起来似乎是对的,但是我们要注意到,数组还有负数,加上一个负数会越加越小。那么这里再多想一步,我们如果不然后面小的那个数加上负数,而是让前面大的那个数加上负数,那么不就可以实现了。但是还有一种情况,就是一正一负,这样要加的次数可能很多,比如前面的数是1,后面的数是-20,那么要加的就很多了。而且这样的来个两三个,我们操作次数就超了。所以我们这里考虑将所有的数都统一成正数或者负数,那么一遍下来,最坏也就操作19次。该如何统一呢,我们注意到一旦能有个数大于20或者小于-20,那么所有不符合正负要求的数跟它操作一次即可。最坏的情况就是从1开始变,那么需要五次,然后现在还剩7次操作,显然需要想考虑下数组中正数和负数的个数,如果一种数的个数小于等于7,那么直接按照上述方法操作就能保证绝对不会超。那么如果两种数的个数超不多呢,那么我们就要考虑省掉那五次操作,而选一个绝对值最大的数将与它正负性不同的数改变一下。这样最多的操作次数为12次,再加上19次刚好31次。另外还要注意到一点,在19次的操作循环中,如果全是正数,那么我们从前往后即可,如果全是负数,需要从后往前。因为对于正数来说,我们修改的是后面的那个数,如果从前往后刚好可以不断比较,但是从后往前就会使本来修改好的顺序又乱了。另外那19次操作我本来准备加个判断优化,但是发现加了之后总有数据过不了,而且还是比较靠后看不到的数据,于是把优化删了,ac了。

#include<bits/stdc++.h>
using namespace std;
int a[100];
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n;
		scanf("%d",&n);
		int mi=30,mx=-30,idx1,idx2,z=0,f=0;
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			if(a[i]>0) z++;
			if(a[i]<0) f++;
			if(a[i]>mx)
			{
				mx=a[i];
				idx1=i;
			}
			if(a[i]<mi)
			{
				mi=a[i];
				idx2=i;
			}
		}
		vector<pair<int,int>>p;
		if(mx<0)//全负
		{
			for(int i=n;i>=2;i--)
			{
				a[i-1]+=a[i];
				p.push_back({i-1,i});	
			}
		}
		else if(mi>0)//全正
		{
			for(int i=1;i<n;i++)
			{
				a[i+1]+=a[i];
				p.push_back({i+1,i});
			}
		}
		else//都有
		{
			if(z>12)
			{
				while(mx<20)
				{
					mx += mx;
					p.push_back({idx1,idx1});
				}
				for(int i=1;i<=n;i++)
				{
					if(a[i]<0)
					{
						a[i]+=mx;
						p.push_back({i,idx1});
					}
				}
				for(int i=1;i<n;i++)
				{
					a[i+1]+=a[i];
					p.push_back({i+1,i});
				}
			} 
			else if(f>12)
			{
				while(mi>-20)
				{
					mi += mi;
					p.push_back({idx2,idx2});
				}
				for(int i=1;i<=n;i++)
				{
					if(a[i]>0)
					{
						a[i] += mi;
						p.push_back({i,idx2});
					}
				}
				for(int i=n;i>=2;i--)
				{
					a[i-1]+=a[i];
					p.push_back({i-1,i});	
				}
			}	
			else
			{
				if(abs(mx)>abs(mi))
				{
					for(int i=1;i<=n;i++)
					{
						if(a[i]<0)
						{
							a[i]+=mx;
							p.push_back({i,idx1});
						}
					}
					for(int i=1;i<n;i++)
					{
						a[i+1]+=a[i];
						p.push_back({i+1,i});
					}
				}
				else 
				{
					for(int i=1;i<=n;i++)
					{
						if(a[i]>0)
						{
							a[i] += mi;
							p.push_back({i,idx2});
						}
					}
					for(int i=n;i>=2;i--)
					{
						a[i-1]+=a[i];
						p.push_back({i-1,i});			
					}
				}
			}
		}
		
		cout<<p.size()<<endl;
		for(int i=0;i<p.size();i++)
		{
			cout<<p[i].first<<" "<<p[i].second<<endl;
		}
	}
}

这样写法代码量太多了,现提供一种代码量比较少的写法。 

#include<bits/stdc++.h>
using namespace std;
int a[40];
vector<pair<int,int>>p;
void change(int i,int j)
{
	a[i] += a[j];
	p.push_back({i,j});
}
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n;
		scanf("%d",&n);
		int z=0,f=0,zi,fi;
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			if(a[i]>0) z++,zi=i;
			if(a[i]<0) f++,fi=i;
		}
		if(z>12)
		{
			while(a[zi]<=20)
			 change(zi,zi);
		}
		if(f>12) 
		{
			while(a[fi]>=-20)//如果不加等号,后面找到的可能不是这个数,而是正负性相反的数,会使操作数增加
				change(fi,fi);
		}
		int mx=0,idx;
		for(int i=1;i<=n;i++)
		{
			if(abs(a[i])>abs(mx)) 
			{
				mx=a[i];
				idx=i;
			}
		}
		if(mx>0)
		{
			for(int i=1;i<=n;i++)
				if(a[i]<0) 
					change(i,idx);
			for(int i=1;i<n;i++)
				change(i+1,i);
		}
		if(mx<0)
		{
			for(int i=1;i<=n;i++)
				if(a[i]>0) change(i,idx);
			for(int i=n;i>1;i--)
				change(i-1,i);
		}
		cout<<p.size()<<endl;
		for(int i=0;i<p.size();i++)
			cout<<p[i].first<<" "<<p[i].second<<endl;
		p.clear();	
	}	
}

ps:好像也没少到哪儿去,哈哈哈。

Earn or Unlock(Problem - D - Codeforces

题目大意:现在有一个数组,排成一排,最开始只有第一个数是被解锁的,我们每次可以进行一下两种操作之一
1.选择一个已解锁的牌, 假设它的数值是v,用它去解锁未解锁部分最前面的v张牌

2.将v计入分数。

操作结束后将这张牌删除。问最后最大能获得多少分数。

思路:第一张牌如果用来解锁,我们假设它能解锁v1,v2,两张牌,然后讨论一下用这两张牌去解锁其它牌产生的效益有什么不同:

显然用v1和用v2是等价的。因为它们能解锁的牌的张数等于它们各自的数值。所以一张牌要么用来解锁v张牌,要么产生v的贡献。假设最后被我们解锁的有k张牌,那么它们就需要花费k-1的代价, 因为第一张牌是默认解锁的。

这题实际上要用动态规划来写,虽然没有直接看出来哪里要规划,但是我们来学习一下这个思路。

定义dp[i]表有没有可能解锁1-i,0表示不可以,1表示可以

如果第i张牌被解锁,那么暴力枚举后面所有的j,用掉i后,j+ai张牌就被解锁。

dp[i]=1//被解锁

for(int j=1;)

 {       

        dp[j+a[i]]=dp[j];//如果j已经被解锁,那么j后面的a[i]张牌就可以因为花费a[i]而被解锁

}

如果前k张牌都被解锁,那么可以产生的价值就是前k张牌的数值和,但是由于解锁需要花掉k-a[1]-1,那么再把这部分减掉就是。另外有超出n的可能,那么我们需要考虑2*n位,超出n的计0.当然第一张牌不用计入。

暴力的话就是这么来写: 

#include<bits/stdc++.h>
using namespace std;
int a[200010],sum[200010],dp[200010];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i];
	dp[1]=1;
	for(int i=1;i<=n;i++)
	{	
		if(dp[i])
		{	
			for(int j=i;j<=n;j++) dp[j+a[i]]=dp[j];
		}
	}
	int ans=0;
	for(int i=2;i<=n;i++)
	{	
		if(dp[i])
		{
			ans = max(ans,sum[i]-i+1);
		}
	}
	cout<<ans<<endl;
}

但是很显然时间复杂度是O(n^2),所以过了五组样例就超时了。

那么就要考虑如何优化,显然思路上没有可以优化的地方了,那么我们来考虑,dp[i]表示的是前i个能不能被解锁,所以只有两种值,可以或者不可以,我们实际表示的时候用的也是01来表示,那么实际的数组不就是一个很长的01串,那么这里我们引入bitset来优化。

首先我们这么来看,因为上面那个嵌套循环实际上是因为用第i位时,前面的都需要被更新,所以需要循环,但实际上每个j往前跳的都是a[i]个,所以我们在一串01串中,让所有位左移a[i],那么不就相当于实现了将当前位的状态转移给前面第a[i]个。而这样只需要进行一次操作,直接省掉了一层循环。显然是可行的,因为我们要的只是第i位的状态。另外要注意,因为我们的a[i]是从前往后遍历的,每次是将dp整体左移a[i],所以后面的a[i],实际上前面定下来的状态不能再由于它而往前更新,就像上面那个循环中,j要从i开始枚举。所以我们可以记录一下,然后将dp[i]置0.

最后同上面一样找遍历获取结果即可。

#include<bits/stdc++.h>
using namespace std;
int a[200010];
long long sum[200010];
bitset<200010> dp,res;
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i];
	dp[1]=1;
	for(int i=1;i<=2*n;i++)
	{
		dp = dp | (dp<<a[i]);
		res[i]=dp[i];
		dp[i]=0;
	}
	long long ans=0;
	for(int i=1;i<=2*n;i++)
	{
		if(res[i])
		{
			ans = max(ans,sum[i]-i+1);
		}
	}
	cout<<ans;
}

这题最核心的有几部分:
首先,需要分析出来我们第一次解锁后,后面无论使用哪个来解锁其他的,实际上都是等价的,所以我们每次解锁用最前面的那个元素。

其次就是状态的定义和更新要想清楚,因为用来开牌或者用来计入结果,它们产生的贡献在数量上是一致的。开到第k张牌,那么说明前面的牌都已经开了,但是由于开到这里会有一些花费,所以总共的贡献就是前面的价值减去到k时的花费。

那么我们只用找到可以到达的位置k即可。那么如何找位置k呢,实际上用a[i]开到的位置应该是最后一个位置+a[i]得到的位置,但是我们如果要直接获取这个位置是很麻烦的,这里的处理就是折中一下,将所有已经解锁的位置都加上a[i]更新一个位置,由于是非负数,所以求和的话后面的一定大于等于前面的,那么实际更新到的位置最远,故而也是最大的,所以不会影响结果。相当于我们只是加了一些冗余进来,并不影响结果,但我们按照这个策略一定可以找到最远位置。只找最远位置是一个点,这个点的区间是都被更新过的,这样就将区间具象化了,区间中的每一个点都被更新了。最难的一步也是在这里,就是状态的表示和更新,并不是恰好更新到点,而是将这段区间都更新。

然后很显然,遍历更新会超时,然后每个点只有两种状态,所以我们直接用bitset优化一下。一次操作,将后面所以状况都更新掉。那么就实现了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值