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