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优化一下。一次操作,将后面所以状况都更新掉。那么就实现了。