Codeforces Round 901 (Div. 2)补题

 Jellyfish and Undertale(Problem - A - Codeforces

题目大意:现有一个炸弹,初始设定的时间是b秒,然后每秒时间减少1,减到0爆炸,我们有n个工具,我们可以用这些工具去修改时间,但是时间上限不能超过a,问最晚什么时候爆炸。

思路:实际就是判断每个工具能产生的效益,如果1+x<=a,那么这个工具可以产生的效益就是x,否则就是a-1,因为我们在1s的时候用它,最多能加到a,那么效益就是a-1,累计一下即可。

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

Jellyfish and Game(Problem - B - Codeforces

题目大意:J有n个苹果,G有m个苹果,每个苹果都有一个价值,他们做k轮游戏,奇数轮时,J可以用自己的苹果去换G的,偶数轮时则相反,当然他们也可以都不操作。问游戏结束后,J手中苹果的价值总数是多少。

思路:这道题的操作数可能很大,所以很容易想到操作数是有规律的,同时,很容易想到最优策略就是用自己最小的去换对方最大的,进而,如果所有数中最小的和最大的分属于两个人的话,实际上就是两个人来回换,那么我们就最小和最大所处位置简单讨论一下即可:

设最小的为mi,最大的为mx,另外如果数组中没有mx,那么就将它里面最大的设为nmx,如果数组中没有mi,那么就将它里面最大的设为nmi.

情况一:(一定会进行操作,因为mi<nmx)

J:mx mi

G:nmx,nmi

1.

J:mx,nmx

G:mi,nmi

2.

J:mi,nmx

G:mx,nmi

3.

J:mx,nmx

G:mi,nmi

...

=>

if(k%2) suma=suma-mx-mi+mx+nmx;

else suma=suma-mx-mi+mi+nmx;

情况二:(一定会进行操作,因为nmi<mx)

J:nmx,nmi

G:mx,mi

1.

J:nmx,mx

G:nmi,mi

2.

J:nmx,mi

G:nmi,mx

3.

J:nmx,mx

G:nmi,mi

...

=>

if(k%2) suma=suma-nmx-nmi+mx+nmx;

else suma=suma-nmx-nmi+mi+nmx;

情况三:(一定会进行操作,因为mi<mx)

J:nmx,mi

G:mx,nmi

1.

J:nmx,mx

G:mi,nmi

2.

J:nmx,mi

G:mx,nmi

...

=>

if(k%2) suma=suma-nmx-mi+nmx+mx;

else suma=suma-nmx-mi+nmx+mi;

情况四:(因为要考虑到可能会不操作)

nmi<nmx

J:nmi,mx

G:mi,nmx

1.

J:nmx,mx

G:mi,nmi

2.

J:nmx,mi

G:mx,nmi

3.

J:nmx,mx

G:mi,nmi

=>

if(k%2) suma=suma-nmi-mx+nmx+mx;

else suma=suma-nmi-mx+nmx+mi;

nmi>=nmx

J:nmi,mx

G:mi,nmx

1.

J:nmi,mx

G:mi,nmx

2.

J:nmi,mi

G:mx,nmx

3.

J:nmi,mx

G:mi,nmx

=>

if(k%2) suma=suma-nmi-mx+nmi+mx;

else suma=suma-nmi-mx+nmi+mi;

至此讨论完全。

#include<bits/stdc++.h>
using namespace std;
int a[100],b[100];
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n,m,k;
		scanf("%d%d%d",&n,&m,&k);
		long long suma=0,sumb=0;
		int mia=1e9+10,mib=1e9+10,mxa=0,mxb=0;
		for(int i=1;i<=n;i++) scanf("%d",&a[i]),mia=min(mia,a[i]),mxa=max(mxa,a[i]),suma+=a[i];
		for(int i=1;i<=m;i++) scanf("%d",&b[i]),mib=min(mib,b[i]),mxb=max(mxb,b[i]),sumb+=b[i];
		suma=suma-mia-mxa;
		sumb=sumb-mib-mxb;
		int mx=max(mxa,mxb),mi=min(mia,mib),nmx,nmi;
		if(mx==mxa&&mi==mia)
		{
			nmx=mxb,nmi=mib;
			if(k%2==0) suma += mi+nmx;
			else suma += mx+nmx;
		}
		else if(mx==mxb&&mi==mib)
		{
			nmx=mxa,nmi=mia;
			if(k%2==0) suma += nmx+mi;
			else suma += nmx+mx;
		}
		else if(mx==mxa&&mi==mib)
		{
			nmx=mxb,nmi=mia;
			if(nmi<nmx)
			{
				if(k%2==0) suma += nmx+mi;
				else suma += nmx+mx;
			}
			else 
			{
				if(k%2==0) suma += nmi+mi;
				else suma += nmi+mx;
			}
		}
		else 
		{
			nmx=mxa,nmi=mib;			
			if(k%2) suma += nmx+mx;
			else suma += nmx+mi;
		}
		printf("%lld\n",suma);
	}
}

Jellyfish and Green Apple(Problem - C - Codeforces)

题目大意:有n块重量相等的苹果,每次可以将任意一块苹果平均分成两块重量相等的苹果,现在需要将这n块苹果平均分给m个人,问最少切几刀,如果无法实现那么输出-1.

思路:首先我们来考虑最坏的情况,将每块苹果都平均分给m个人,一刀可以得到2块,2刀3块(不均等),3刀4块,4刀5块(不均等)...所以要想均等来分的话,人数必须是2^k(k=0,1,2,...),否则就不可能实现。那么我们现在举个例子,来讨论最优策略

将3块苹果分给8个人:

那么每个人分到的是3/8块,那么最优的解法是分给每个人一块1/4和一块1/8的。

因为切一刀多一块,所以要想操作数最小,那么每个人分到的块数就要最少,比如3/8也可以拆成3个1/8的,但是1/4可以合并,那么就要将它们合并,进而少切几刀。另外我们可以发现,合法的情况实际上都可以拆成几个1/2^k的和。这样可以保证每个人分到的块数是最少的。

那么现在的问题就是如何用代码表示像3/8这种数该如何拆。实际上我们注意到,我们只需要考虑它能拆成几块就可以了,那么我们从二进制的角度来看3=(11),8=(1000),我们大胆猜测与3中1的个数有关,我们推到5试试,5=(101),5/8=4/8+1/8=1/2+1/8可以,推到7,7=(111),7/8=4/8+2/8+1/8=1/2+1/4+1/8可以。那么我们只用先将n/m化简,然后找到化简后的分子中二进制1有几个,那么就是几块,然后m个人共有多少块,块数-1即是刀。对了,需要考虑到n>m的情况,那么实际上应该是余数去除。而且要注意到,我们实际上是对n/m化简后的数来找是否可以,所以需要化简一下,找它们的最大公因数即可实现化简。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int gcd(int a,int b)
{
	return b?gcd(b,a%b):a;
}
int get1(int d)
{
	int c=0;
	while(d)
	{
		if(d%2) c++;
		d /= 2;
	}
	return c;
}
signed main()
{
	int t;
	scanf("%lld",&t);
	while(t--)
	{
		int n,m;
		scanf("%lld%lld",&n,&m);
		n=n%m;
		int g=gcd(n,m);
		int c=get1(m/g);//这一步是为了判断是否是2^k,要注意,我们实际分的是n/m化简后的结果,所以要考虑会不会化简后就可以了
		if(!n) printf("0\n");//直接可以均分
		else if(c>1) printf("-1\n");
		else
		{
			c=get1(n/g);
			//将n分给m个人,每人c块
			int ans=m*c-n;//总共8块,现有d块,还需8-d块,
			printf("%lld\n",ans);
		}
	}
}

Jellyfish and Mex(Problem - D - Codeforces

题目大意:我们首先定义mex作为不存在于数组的最小自然数。现在有一个大小为n的数组a[]和一个初值为0的数m,我们需要进行n次操作,每次操作从数组中删除一个数,然后将此时数组的mex加给m,输出m最后的值。

思路:显而易见,当mex等于0后,无论怎么操作,我们的mex都是0,不可能再小了,所以实际有效的是mex变成0之前的操作。mex怎么变成0呢,首先如果原数组的mex就是0,那么直接输出0就可,不需要再讨论什么。但如果原数组的mex不是0,那么就要考虑,怎么让它变成0,有两种策略,一种就是直接去删除0,删到它变成0为止,还有一种策略是删除0-mex之间的数来中转一下,为什么要中转,因为mex和c[0](c[i]表示i的个数可能过大,转移一下,可以让c[0]去乘另外一个数,能够让这个值小一点。)所以现在就是要来考虑如何从两种情况中找出最小值。

我们很容易找出mex,如果:
mex->(mex-1):只有直接变这一种情况,没有数可以被用来中转

mex->(mex-2):可以直接变,也可以用mex-1来中转

...

我们可以发现,我们实际上是可以算出来mex->(mex-i)的过程中对结果的贡献:

mex->(mex-1):mex*(c[mex-1]-1) 令其为dp[1]

mex->(mex-2):mex*(c[mex-2]-1) , mex*(c[mex-1]-1)+(mex-1)*(c[mex-2]-1) (两者找最小值)

                        mex*(c[mex-2]-1) , dp[1]+(mex-1)*(c[mex-2]-1) 我们将最小值设定为dp[2]

mex->(mex-3):mex*(c[mex-3]-1),dp[2]+(mex-2)*(c[mex-3]-1),dp[1]+(mex-1)*(c[mex-3]-1)

...

以此类推,不就可以得到mex->0的过程中对m的贡献。为0之后的贡献就全为0了,自然不用再考虑。

另外要注意到,我们举个例子来说明吧:

0 0 0 1 2 3 4 5

mex初值是6,

按照上述计算,变成5,对结果的贡献是0,但实际上,我们删除完就要读一次值,所以这个贡献应该是5.可以这么理解,我们上面得到了在等于这个值之前需要的花费,在等于这一瞬实际上也是有一个花费的,我们需要把它加上。才能保证后面的计算不出问题,因为像这种只有一个的,我们将mex变成它,实际上还是有花费的,因为删除和取值联动。

#include<bits/stdc++.h>
using namespace std;
int n,a[200010],dp[200010],b[200010];
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d",&n);
		map<int,int>mp;
		for(int i=1;i<=n;i++) scanf("%d",&a[i]),mp[a[i]]++;
		int s=0;
		for(auto it:mp)
		{
			if(s!=it.first) break;
			s=it.first+1;
		}
		if(!s)//数组中没有0
		{
			printf("0\n");
		}
		else
		{
			for(int i=1;i<=s;i++) //mex-i
			{
				int c=mp[s-i]-1;
				dp[i]=s*c;
				for(int j=1;j<i;j++)//通过mex-j来中转。
				{
					int mex=s-j;
					dp[i]=min(dp[i],dp[j]+c*mex);
				}
				dp[i] += s-i;
			}
			printf("%d\n",dp[s]);
		}
	}
	
}

ps:我们再换个角度理一遍思路:

从头分析,如果要使结果变成0,那么可以直接上来就删0,也可以中转一下,中转肯定是用0-mex之间的数来中转,大于mex的数是没有考虑的必要的。
那么0-mex中间有那么多的数,我们到底用哪个来中转,或者用哪几个来中转呢?情况太多了,于是想到能不能用dp来累计状态。
我们就需要找到临界的状态。
首先要明白,我们想要的状态是什么,既然最终想要到0,那么就以0来分析状态,即0为mex时,m的值,或者说对m的贡献。
我们要用0-mex之间的数来进行转移,转移的意义是什么呢,很显然就是mex->x->0,那么mex->x不也即x的状态,所以就得到了x的状态+x->0的贡献即为从x处转移得到的贡献,x->0该如何计算呢,这个就很简单了(c0-1)*x即可。现在就要考x的状态怎么计算,很显然,mex->x可能也需要转移,那么就是用x-mex之间的数来转移,同样有很多情况,那么我们假设选了一个之后,再往下看,最终推到mex->mex-1,这种情况没办法转移了,只能直接变,那么我们就先把dp[mex-1]算出来,然后既然不能再往前推了,我们就往后看mex->(mex-2),诶,这个就只有两种情况,直接转,或者用mex-1中转一下,只有两种十分明确的状态,计算也很容易,然后我们要的肯定是最小值。那么再往前推,mex-3,它有3种情况:mex直接转,mex-1转,mex-2转,那么不也是用已知的很明确的状态去计算(当然这里回想,是mex->(mex-2)->(mex-3),还是mex->(mex-1)->(mex-2)->(mex-3),但,显然我们的mex-2在计算的时候已经做出了决策,我们取得是小的那个,所以mex->(mex-2)用的是哪种方法我们并不在意,我们在意的只有(mex-2)->(mex-3)的过程)。那么后面的以此类推不就可以得到了。

对于动态规划类的题目,结果对应的状态个方案太多了,那么求解实际上是一个不断减少状态的过程,一直推,推到状态减少到能算为止。所以我们写此类题,首先考虑结果可以由哪些方案得到,我们从中挑选一个方案后,再考虑这个方案可以由什么方案得到,一直往后推,往下找,直到我们可以计算出需要的方案为止。抓住结果,逆向推导。当结果需要从多种方案中获得一个值(max,min,count等)时,就要开始通过往前找来压缩方案数,压缩到可以算为止,这也就是所谓的动态规划。

ps:写题的时候,经常会有灵光乍现的时候,这是过往的积累在潜意识中整合的结果,我们需要做的未必是非要给这次的灵光乍现找一个来源和解释,我们只需要去复盘这个灵感,抽丝剥茧的理清楚,在此基础上举一反三,而不是非要追根溯源。

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值