Divide by Zero 2021 and Codeforces Round #714 (Div. 2)A-E题解
比赛链接:https://codeforces.com/contest/1513
E少想了一种情况,赛中没过有点可惜。
A题
相关tag:构造
题意:
给定1-n(n<100)共n个整数,让你构造出一个排列方式,使得恰好有k个位置满足其左侧和右侧最近的那个数都比它的值小。
思路:
方便说明起见,将题目所说的位置称作“峰顶”
首先如果有两个下标i和j满足题目的条件,即a[i]>a[i-1]且a[i]>a[i+1],a[j]>a[j-1]且a[j]>a[j+1],则i和j必定不相邻。因为比如j=i+1,那么a[i]>a[i+1]和a[j]>a[j-1]这两个条件就冲突了。
那么一个长度为n的排列方式,除去第一个和最后一个位置,中间的n-2个位置我们间隔1个下标位置放“峰”。n-2个位置最多能放(n-2+1)/2个"峰顶",因此长度n最多能有(n-1)/2个"峰顶"。
k必须满足k<=(n-1)/2才有满足的构造,否则直接输出-1代表无解
我们确定了哪些位置要作为“峰顶”后,接着是构造过程。
我们可以用这种简便的构造方式:
“峰”的位置,我们取n个数中最大的k个,依次放置。这样子保证了峰顶的值必定大于其左右两边的数
剩余的位置,我们将剩下的1到(n-k)依次从左到右放置,这样子保证了除了我们想要构造的峰顶外,其他位置不存在峰顶,原因在于其他位置我们是从小到大排列的,只有峰的一侧,无法构成峰
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f //2139062143
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int maxn=1e6+7;
const double eps=1e-6;
const int mod=1e9+7;
bool flag[107];
int num[107];
int main(){
IOS
int t;cin>>t;
while(t--){
int n,k;cin>>n>>k;
int lim=(n-1)/2;
if(lim<k) cout<<-1<<endl;
else{
memset(flag,0,sizeof(flag));//flag[i]标记第i个位置是否被使用
for(int i=0;i<k;i++){//在下标1,3,5,7....依次放置k个最大的数
flag[i*2+1]=1;
num[i*2+1]=n-i;
}
int cas=1;
for(int i=0;i<n;i++){//剩下的数从小到大,从左往右依次放进去
if(!flag[i]){
num[i]=cas++;
}
}
for(int i=0;i<n;i++) cout<<num[i]<<' ';
cout<<endl;
}
}
}
B题
相关tag:构造,排列组合,位运算
题意:
给定n(n>1)个整数,你可以任意排列他们的顺序,询问有几种排列方法(注意如果给定的是[3,3]是有两种排列方式的,即值相同的数也被视作不一样的)满足如下条件:
对于任意的下标i在[0,n-1]内,下标1到下标i的数位与运算得到的结果,与下标i+1到下标n的数位于运算得到的结果相等。
思路:
结论1:与运算的结果必定不大于参与运算的所有数中的最小值。
我们可以推出一个结论2:放在最前面和最后面的数必然是这n个数中最小的那个值。
证明:反证法,假设放在最前面的不是最小的那个值,设为Min,而是一个值x满足x>Min。那么我们下标i取0的时候,前面部分的与运算结果是x,而后面部分的与运算结果由结论1可知必定<=Min,与x不相等。
那么最前面和最后面的数都是最小值就可以了么,注意结论1是不大于而不是等于最小值。因此我们还要再加个条件,作为结论3:这n个数的位与运算结果必须等于最小值Min
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f //2139062143
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int maxn=2e5+7;
const double eps=1e-6;
const int mod=1e9+7;
ll n;
ll num[maxn];
int main(){
IOS
int t;cin>>t;
while(t--){
cin>>n;
ll cnt=0;//记录有几个最小值
ll Min=llINF;
ll temp;//计算n个数的位与结果
for(int i=0;i<n;i++){
cin>>num[i];
if(i) temp&=num[i];
else temp=num[i];
if(num[i]<Min){Min=num[i];cnt=1;}
else if(num[i]==Min) cnt++;
}
if(cnt<2||temp!=Min) cout<<0<<endl;
else{
ll ans=cnt*(cnt-1)%mod;//最小值里挑两个放在开头结尾
for(int i=2;i<=n-2;i++) ans=ans*i%mod;//剩下的在中间任意排列,即(n-2)!
cout<<ans<<endl;
}
}
}
C题
相关tag:线性dp
题意:
给定一个整数值n和操作次数m。
每次操作,都把n按照十进制每一位拆出来,然后每个值+1,作为n的新值。
询问n被操作m次后有多少位
思路:
直接看代码注释吧.
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f //2139062143
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int maxn=1e6+7;
const double eps=1e-6;
const int mod=1e9+7;
int n,m;
ll dp[maxn][10];//dp[i][j]代表从数值9作为起始值,进行i次操作后,数值9有多少个
ll len[maxn];//len[i]代表9被操作i次后的长度
int main(){
IOS
dp[0][9]=1;
for(int i=1;i<=200000;i++){
for(int j=1;j<=9;j++) dp[i][j]=dp[i-1][j-1];//数值1-9都可以从上次操作得到的0-8依次+1转移过来
dp[i][0]=dp[i-1][9];//数值0是从9+1转移过来
dp[i][1]=(dp[i][1]+dp[i-1][9])%mod;//9+1还会得到一个1
for(int j=0;j<=9;j++) len[i]=(len[i]+dp[i][j])%mod;//累加当前0-9总共有几个,即第i次操作后的长度
}
int t;cin>>t;
while(t--){
cin>>n>>m;
ll ans=0;
while(n){
int temp=n%10;//temp为当前处理的十进制位
n/=10;
if(temp+m<10) ans=(ans+1)%mod;//如果当前位上的值加上操作次数小于10,则只有1位
else ans=(ans+len[temp+m-9])%mod;//如果大于等于9,等价于对9操作temp+m-9次操作得到的结果
}
cout<<ans<<endl;
}
}
D题
相关tag:结论,贪心
题意:
给定一个长度为n的整数数组a[],以及一个整数p
给定一个有n个点的图,这些点用整数值0到n-1标记,以及若干条有权边,要求这张图最小生成树的总权值大小。
图中的边有两种:
1.如果第i个点和第j个点,对应数组a中下标i和之间的所有的数,他们的最小值等于他们的gcd值。那么第i个点和第j个点之间有一条边,权值为gcd值。
2.如果i+1=j,那么第i个点和第j个点之间有一条权值为p的边。
思路:
求最小生成树。然而这是个假图论题。
首先注意到第2种边已经把n个点连成了一棵树,已经是一颗生成树了,但是权值不一定是最小的。我们可以在这个基础上去想。是否可以选择第一种边来使得整棵树的权值降低。
我们可以贪心选择第1种边比p小的,替代掉原本的第2种边。但是如何贪心选择当前还能选择的权值最小的边,是我们需要解决的问题。
注意到权值是下标i到j之间最小的最小值,那么我们可以对数组a[]的值从小到大排序依次考虑,利用结构体记录下数值在a[]中所在的位置。即为依次考虑当前可能的权值最小的边了。
接下来要处理的问题是,确定了这条边的权值(即gcd和最小值)后,如果确定边界i和j,边界i和j构成的范围同样要贪心选择尽可能大的范围。
假设我们当前选择的是a[k]这个数,
那么我们可以通过l=k,while(l>1&&!flag[l-1]&&a[l-1]%node[i].num==0) flag[l–]=1找到最左侧的边界,这些范围内的数均能整除a[k],
同理通过r=k,while(r<n&&!flag[r+1]&&a[r+1]%node[i].num==0) flag[r++]=1找到最右侧的边界。
同时用flag[]数组标记上被包括的所有点,这样每个点只会被记录一次,确保了我们的算法是O(n)的
由此需要执行的操作均已被实现。
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f //2139062143
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int maxn=1e6+7;
const double eps=1e-6;
const int mod=1e9+7;
ll n,p;
ll a[maxn];
bool flag[maxn];
struct Node{
ll num;
int tar;
};
vector<Node>node;
bool cmp(Node a,Node b){
return a.num<b.num;
}
int main(){
IOS
int t;cin>>t;
while(t--){
cin>>n>>p;
node.clear();
for(int i=1;i<=n;i++){
cin>>a[i];
flag[i]=0;
node.push_back({a[i],i});
}
sort(node.begin(),node.end(),cmp);
ll ans=(n-1)*p;//只选择第2种边得到的权值
for(int i=0;i<n;i++){
if(node[i].num>=p) break;//如果当前的值已经不比p小了,已经没有继续执行的意义
if(flag[node[i].tar]) continue;//如果当前的值已经被扫过,则跳过
int l=node[i].tar,r=l;//左右边界
flag[l]=1;
while(l>1&&!flag[l-1]&&a[l-1]%node[i].num==0) flag[l--]=1;//左右边界取尽可能大
while(r<n&&!flag[r+1]&&a[r+1]%node[i].num==0) flag[r++]=1;
ans-=(r-l)*(p-node[i].num);//取边后能减少的权值
}
cout<<ans<<endl;
}
}
E题
相关tag:构造,思维
题意:
给定n个整数,你可以按照任意顺序将他们排列成一个数组a[]。(注意这里相同的数值被视作同一个数,比如[3,3]只有1种排列方案而不是2种)
排列成一个数组a[]后,如果这个数组可以通过若干次操作后使得这个数组里的每个数值都相同,且最大消耗和最小消耗相同,则称这个数组为平衡数组。
你可以执行的操作如下:
选择两个下标i和j,令a[i]的值-x,令a[j]的值+x。本次操作的消耗为x
×
\times
×|j-i|。
另外还有一条特殊规定,对于同一个下标值i,a[i]只能被执行减法操作,或者只能被执行加法操作,也就是在第一次对a[i]操作后,a[i]可以执行的是加法还是减法操作就已经是固定不变的了。
现在问有多少种不同的平衡数组构造方案。
思路:
首先我们的操作是不影响整个数组的所有值的和的,因此所有数的和必须满足能整除n。我们把sum/n记作lim。
我们可以把所有的数分为三类:
1.大于lim的值,这些值只能做减法操作,放到vector high里
2.等于lim的值,这些值不能进行操作,放到vector base里
3.小于lim的值,这些值只能做加法操作,放到vector low里
接着我们把需要考虑的问题分类一下,先去考虑简单的情况:
1.high和low的个数均为0,也就是说base的个数是n,一开始所有数就是都相等的,此时易得就1种构造方案。
2.high或者low的个数至少有一个为1,那么任意排列都是满足要求的,因为此时每次操作的消耗是固定的。比如low当中只有一个下标k,那么我们每次操作的下标i和j中必须有一个是k,且对每个数来说最后是要变为lim,需要修改的值也是固定的。
此时的方案数,我们先在n个位置里挑选1个位置给唯一的high(或者low)摆放,方案情况C(1,n),再分配剩下的n-1个位置给low(或者high)和base摆放。我们可以通过对low排序后,for一遍找出每个数出现了几次,依次利用组合数计算。
例如low有[3,3,4,5,5,5]6个数,我们可以从左往右扫过去,发现了3出现了两次,方案数乘上C(2,6),再接着扫剩下4个数,发现4出现了1次,方案数乘上C(1,4),再继续发现5出现了3次,方案数乘上C(3,3)。
3.high或者low的个数均超过1个。
此时我们就需要分析一下,什么时候会出现存在不同的消耗值。
观察后我们会发现,如果有两个high的下标h1和h2,有两个low的下标l1和l2,他们满足l1<h1<h2<l2,且他们与lim的差值均为x。
那么如果选择从h1拿x的值到l1,从h2拿x的值到l2,总消耗为x
×
\times
×(|h1-l1|+|h2-l2|)=x
×
\times
×(l2-h2+h1-l1)
而如果选择从h1拿x的值到l2,从h2拿x的值到l1,总消耗为x
×
\times
×(|h1-l2|+|h2-l1|)=x
×
\times
×(l2-h1+h2-l1)
注意到这两种方案的消耗是不同的。
归纳下就会发现,如果有两个low(或者high)的下标,在他们的左侧和右侧各有一个high(或者low)的下标,那么此时就会存在不同的操作消耗。而不存在这种情况的构造,即为操作消耗都相同的情况了。
那么不存在这种情况,实际上就是等价于,low的下标和high的下标各自在整个数组的左侧或者右侧,也就是两个部分的下标范围不存在交叉的部分。因为我们可以分别计算出low和high自身排列的方案数,乘2作为low在左侧high在右侧或者low在右侧high在左侧两种情况。再乘上n个位置里要选择high.size()+low.size()个位置摆放这些数,结果再乘上C(low.size()+high.size(),n)。(因为还有base的部分要排列,所以还要乘上这轮选择)。
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f //2139062143
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int maxn=1e6+7;
const double eps=1e-6;
const int mod=1e9+7;
ll num[maxn];
vector<ll>low,high;
ll qpow(ll a,ll p){
ll ret=1;
while(p){
if(p&1) ret=ret*a%mod;
p>>=1;
a=a*a%mod;
}
return ret;
}
ll C(ll a,ll b){//求C(a,b)组合数,a<b
ll A=1,B=1;
for(int i=0;i<a;i++){
A=A*(b-i)%mod;
B=B*(i+1)%mod;
}
return A*qpow(B,mod-2)%mod;
}
ll cal(vector<ll>&num){
ll ret=1;
sort(num.begin(),num.end());
ll now=num.size();//记录当前还有几个数待选择
for(int i=0;i<num.size();){
int j=i;
while(j+1<num.size()&&num[j+1]==num[i]) j++;
ret=ret*C(j-i+1,now)%mod;
now-=(j-i+1);
i=j+1;
}
return ret;
}
int main(){
IOS
ll n;cin>>n;
ll tot=0;
for(int i=0;i<n;i++){
cin>>num[i];
tot+=num[i];
}
if(tot%n) cout<<0<<endl;//如果tot无法被n整除直接输出0
else{
ll lim=tot/n;
for(int i=0;i<n;i++){
if(num[i]>lim) high.push_back(num[i]);
else if(num[i]<lim) low.push_back(num[i]);
}
if(low.size()==0) cout<<1<<endl;//此时所有数均为lim,只有1种构造
else{
sort(low.begin(),low.end());
sort(high.begin(),high.end());
ll ans;
if(low.size()==1) ans=n*C(high.size(),n-1)%mod*cal(high)%mod;
else if(high.size()==1) ans=n*C(low.size(),n-1)%mod*cal(low)%mod;
else ans=2*cal(low)%mod*cal(high)%mod*C(low.size()+high.size(),n)%mod;
cout<<ans<<endl;
}
}
}