A.Array and Peaks:
题目链接:https://codeforces.ml/contest/1513/problem/A
题目大意:
有t组数据,每一组数据给定n,k,要求求出一个长度为n的排列,使得这个排列有k个峰,如果不能有k个峰则输出-1。峰的意义是,如果a[i]是峰,那么一定满足a[i]>a[i-1] && a[i]<a[i+1]
。
数据范围满足:1<=t<=100,1<=n<=100,0<=k<=n。
题目分析:
1.我们发现如果将峰数为0的排列:1,2,3,······,n-1,n。除了1以外,每两项进行一个交换,那么就会形成一个峰。例如我们选择4,5,则排列变成:1,2,3,5,4,······,n-1,n。这样就形成了一个峰。
2.我们发现,如果我们选择两两不同的数对进行交换,这样形成的峰是不会互相影响的。比如选择(2,3)和(4,5),则排列变成:1,3,2,5,4,6,······,n-1,n。这样就形成了两个独立的峰。所以只要我们选取除了1以外,x个两两不同的数对,那么就可以形成x个峰。
3.此时还有一个问题,那就是当题目要求形成k个峰可行时,以这样的方式形成形成的峰一定有k个吗?因为峰大于左右两边的值的原因,所以每三个数只能由1个峰。长度为n的排列,最多能有(n-1)/2个峰。而我们在1中生成峰的方法同样最多能生成(n-1)/2个峰。所以方法正确。
正解程序:
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long ll;
const ll maxn=110;
ll num[maxn],T;
int main()
{
scanf("%lld",&T);
while(T--)
{
ll n,k,times=0,pos=1;
scanf("%lld%lld",&n,&k);
num[1]=1;
for(ll i=2;i<=n;i+=2)
{
if(k==0)//等于0则不需要制造峰
break;
if(i+1<=n)//如果i已经为n了,则没有办法制造出峰
{
++times;
num[i]=i+1;//交换相邻两项制造出峰
num[i+1]=i;
if(times==k)
{
pos=i+2;
break;
}
}
}
for(ll i=pos;i<=n;i++)//将后续部分保持原样
num[i]=i;
if(times!=k)//如果没有制造出k个峰,输出-1
printf("-1\n");
else
{
for(ll i=1;i<=n;i++)
printf("%lld ",num[i]);
printf("\n");
}
}
return 0;
}
B.AND Sequences:
题目链接:https://codeforces.ml/contest/1513/problem/B
题目大意:
有t组数据,每组数据给定一个长度为n的数组,问有多少种方式可以将这个数组重新排列之后满足:a1=a2&a3&······&an-1&an,a1&a2=a3&······&an-1&an,······,a1&a2&a3&······&an-1=an。
数据范围满足:1<=t<=1e4,2<=n<=2e5,0<=ai<=1e9。
题目分析:
1.首先,我们挨个分析每一个式子:
- a1=a2&a3&······&an-1&an,很容易知道a1=a2&a3&······&an-1&an=a1&a2&a3&······&an-1&an
- a1&a2=a3&······&an-1&an,很容易知道a1&a2=a3&······&an-1&an=a1&a2&a3&······&an-1&an
- a1&a2&a3&······&an-1=an,很容易知道a1&a2&a3&······=an-1&an=a1&a2&a3&······&an-1&an
2.所以很显然,要让这个一连串的式子成立,只有当a1&a2=a3&······&an-1&an=a1&a2&a3&······&an-1&an 等于a1和an时才能成立。而且,除了首尾两个数以外,其他的数具体处于什么位置并不重要。
3.所以我们首先算出last=a1&a2&a3&······&an-1&an,然后判断last是否在a1到an中出现过至少两次。则最终的答案就是
C
t
i
m
e
s
[
l
a
s
t
]
2
C_{times[last]}^{2}
Ctimes[last]2*
A
n
−
2
n
−
2
A_{n-2}^{n-2}
An−2n−2。
正解代码:
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long ll;
const ll maxn=2e5+10;
const ll mod=1e9+7;
ll num[maxn],fac[maxn],T;
int main()
{
scanf("%lld",&T);
fac[0]=1;
for(ll i=1;i<=maxn-10;i++)//计算阶乘,用于求出排列数
fac[i]=fac[i-1]*i%mod;
while(T--)
{
ll n,flag,last,times=0;
scanf("%lld",&n);
for(ll i=1;i<=n;i++)
{
scanf("%lld",&num[i]);
if(i==1)//计算所有数的与
last=num[i];
else
last&=num[i];
}
for(ll i=1;i<=n;i++)//判断last出现了多少次
if(last==num[i])
times++;
if(times>1)//如果last至少出现了2次
printf("%lld\n",times*(times-1)%mod*fac[n-2]%mod);//计算答案
else//否则不可能
printf("0\n");
}
return 0;
}
C. Add One:
题目链接:https://codeforces.ml/contest/1513/problem/C
题目大意:
有t组数据,每组数据给定两个数n和m。定义一次操作为将n的每一位加1,如果某一位变成了10,则在后续的处理中当成两位来计算。例如,9进行两次操作即为21。现要求对n进行m次操作,询问m次操作之后,最后的数的长度为多少。
数据范围满足:2<=t<=2e5,1<=n<=1e9,1<=m<=2e5。
题目分析:
1.很显然,这个数n的每一位都是相互独立的,所以我们单独考虑每一位进行d次操作之后的长度,最后将每一位的答案加起来就是最终的答案。
2.如果将某一个数变成0~9中的任何一个都是不需要增加的,或者说最后的长度都是1。
3.所以考虑后续大于等于10的情况,我们将1每一次操作的变换结果打印出来,观察打表数据之后我们发现,第i次变换的结果是第i-9和第i-10次变换的拼接的结果。如图所示:
4.所以变换为i的长度就是变换为i-9和变换为i-10的长度之和,我们用递推的方式来进行这一过程即可。
正解代码:
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long ll;
const ll maxn=3e5+10;
const ll mod=1e9+7;
ll dp[maxn],T;
int main()
{
for(ll i=0;i<=9;i++)//变成0-9长度仍然是1
dp[i]=1;
for(ll i=10;i<=maxn-10;i++)//打表所得的结论
dp[i]=(dp[i-9]+dp[i-10])%mod;
scanf("%lld",&T);
while(T--)
{
ll n,m,ans=0;
scanf("%lld%lld",&n,&m);
while(n>0)//按位计算答案
{
ll temp=n%10;
ans=(ans+dp[m+temp])%mod;
n/=10;
}
printf("%lld\n",ans);
}
return 0;
}
D. GCD and MST
题目链接:https://codeforces.ml/contest/1513/problem/D
题目大意:
有t组数据,给定n和p,以及一个长度为n的a序列。这些序列上的数作为一个个顶点构成一张图。边的形成条件为:
- 如果一段区间[i,j]满足:gcd(a[i],a[i+1],······,a[j-1],a[j])=min(a[i],a[i+1],······,a[j-1],a[j]),则i,j之间存在一条长度为min(a[i],a[i+1],······,a[j-1],a[j])的边。
- 如果i+1=j,则i,j之间存在一条长度为p的边
题目要求求出这个图的最小生成树的边权和。
题目分析:
1.假设图已经建立好了,那么一种最简单的生成树的方式就是使用所有i和i+1的边。可是,这个树并不一定是最小生成树。
2.我们想到了最小生成树的kruskal算法,从小到大枚举边权进行添加。边权的值即为:min(a[i],a[i+1],······,a[j-1],a[j])。如果边权w>=p,那么显然不需要考虑。
3.但在这个地方,我们不采取加边的方式,而是采用替代的方式,因为我们已经有了一个现成的树了,所以用新的边替代使用1中的方法构造出来的边。很明显,为了不出现环,每一条边只能被替代一次。
4.最后的困难就是解决边的问题。我们可以把a序列从小到大排序,然后从小到大枚举a。假设新的边的边权就是a[pi],然后从pi向左右拓展,判断是否满足题目构成边的条件(gcd和min的条件)。如果满足且这条边还没被替代过,则用这个去替代。否则就尝试下一条边。
5.4中贪心的方法成立的原因无需赘述,就是kruskal算法的原理。
正解代码:
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long ll;
const ll maxn=2e5+10;
struct node
{
ll num;
ll pos;
}point[maxn];
ll a[maxn],n,p,ans;
bool vis[maxn];
bool cmp(node x,node y)
{
if(x.num!=y.num)
return x.num<y.num;
return x.pos<y.pos;
}
int main()
{
ll T;
scanf("%lld",&T);
while(T--)//原本假设所有的边都是i和i+1构成的
{
memset(vis,false,sizeof(vis));//vis表示i到i+1的边是否被别的边所替代,一条边只能被替代一次
ans=0;
scanf("%lld%lld",&n,&p);
for(ll i=0;i<n;i++)
scanf("%lld",&a[i]);
for(ll i=0;i<n;i++)
{
point[i].num=a[i];
point[i].pos=i;
}
sort(point,point+n,cmp);
ll times=0;
for(ll i=0;i<n;i++)//以point[i].num为gcd,类似kruskal的思路
{
ll val=point[i].num;
ll now=point[i].pos;
if(val>=p)//大于等于p则可以直接用p连接
break;
while(now>0)//向左拓展
{
if(vis[now-1] || a[now-1]%val!=0)
break;
vis[now-1]=true;
ans+=val;
now--;
}
now=point[i].pos;
while(now<n-1)//向右拓展
{
if(vis[now] || a[now+1]%val!=0)
break;
vis[now]=true;
ans+=val;
now++;
}
}
for(ll i=0;i<n-1;i++)//如果i和i+1之间仍然需要一条边,则添加
if(!vis[i])
ans+=p;
printf("%lld\n",ans);
}
return 0;
}