Codeforces Global Round 10 A-E题解
//写于大号rating值2075/2184,小号rating值1887/1887
//这几天刚回家,又鸽了一场,到月底前都要练车,可能要接着鸽下去
A题
简单思维
给定一个长度为n的只包含正整数的数列,你每次操作可以选择相邻的两个值不相等的数字,把它们删去后再把他们的和放回原位置,也就是说每次操作后,数列的长度会减一。
现在需要你输出最后能得到的最短的数列长度。
首先,如果整个数列全部只有一个数字,那么我们没有办法对这个数列做任何操作,只能输出原数列长度n。
当数列包含两个以上的数字时,我们假设初始时候,数列中最大的数字值为X,由于这个数列中除了X外还存在别的数字,因此必然存在一个X,它的相邻位置有不等于X的数字。
我们第一次操作把这个X与和它相邻的不等于X的数字合并后,得到一个新的值Y是大于X的,也就是说,数列中不再存在与Y相等的数,我们之后的操作,每次都用这个最大的数字去和其他数字合并即可,最后必定能使得数组长度变为1。
因此直接判断一下,原数列是否只存在一个数字,如果只有一个数字则输出n,否则输出1。
#include<bits/stdc++.h>
#define ll long long
#define int long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
int n;
cin>>n;
vector<int>num(n);
for(auto &x:num) cin>>x;
bool flag=1;
for(int i=0;i+1<n;i++)
if(num[i]!=num[i+1]) flag=0;
if(flag) cout<<n<<endl;
else cout<<1<<endl;
}
}
B题
简单思维
给定一个数列(可能存在负数),需要你输出k次操作后,数列中的每个值是多少。
每次操作,当前数列中最大值为d,把这个数列中每个数的值,都变为d减去原数值。
观察第二组样例
原数列中存在负数,最大值为5。
第一次操作后,原数列变为
0 6 1 3 5
可以注意到,原本的负数变为了正数,且绝对值最大的负数变为了当前的最大值,而原本的最大值5的位置变为了0。
也就是说,第一次操作后,我们必定会的得到一个0,并且会使负数全部消失。
当前的数列为仅包含正数和0的数列,在接下来的几次操作后,数列变为
6 0 5 3 1
0 6 1 3 5
6 0 5 3 1
0 6 1 3 5
…
容易注意到这是个以两次操作为一个循环的形式。原因在于,假设当前数列最大值为d,每次操作后,原本数列中的0会变成d(由于数列中不存在负数,d即为此时的最大值),而原数列中的d会变为0。那么下次操作时,数列中的最大值仍然为d,且仍然存在0。
容易观察到这已经构成了两次操作一次循环的条件。
操作数k>=1,因此我们直接对原数列进行一次操作,除去负数且得到一个0。之后由于是两次操作一次循环,所有根据k是否为偶数,决定是否要再进行一次操作即可。
#include<bits/stdc++.h>
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
void change(vector<ll> &num)
{
ll M=-llINF;
for(auto x:num) M=max(M,x);
for(auto &x:num) x=M-x;
}
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n,k;
cin>>n>>k;
vector<ll>num(n);
for(auto &x:num) cin>>x;
change(num);
if(k==1)
{
for(auto x:num) cout<<x<<' ';
cout<<endl;
}
else
{
if(k%2==0) change(num);
for(auto &x:num) cout<<x<<' ';
cout<<endl;
}
}
}
C题
贪心,施行
给定一个数列,每次操作你可以选择连续的一段区域,把这个区域的每个值都加上1,问你最少需要多少次操作后,可以使得整个数列变为值不下降的数列。
这里理解下面举得这个例子就可以了
对于数列3 1 2 1 5
我们把这个数列的值看成高度的话,容易发现有两个“谷底”,位置2和位置4的两个1。
如果我们分开操作这两个谷底:
第一次操作后:3 2 2 1 5
第二次操作后:3 3 3 1 5
此时第一个谷底已经被“填平”
第三次操作后:3 3 3 2 5
第四次操作后:3 3 3 3 5
这样是需要4次操作。
然而最优的方案是下面这种:
第一次操作后:3 2 2 1 5
第二次操作后:3 2 2 2 5
第三次操作后:3 3 3 3 5
对于第一个谷底来说,前面出现的最大数字是3,那么最少需要3-1=2次操作才能填平,同理第二个谷底的前面出现的最大数字是3,也需要2次操作才能填平。
但是正如最优方案所演示的那样,对于第二个谷底,原数列3 1 2 1 5,位置3的2是左侧的第一个峰顶,我们只要把位置4的1填到2即可“搭上填平前面谷底的顺风车”。
由此得到一个结论,对于每个谷底,我们只要累加,它左侧的第一个峰顶的值减去当前谷底的值,即为最终答案。
#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
vector<ll>num(n);
for(auto &x:num) cin>>x;
ll ans=0;
ll M=num[0],cas=num[0];//M记录当前位置左侧第一个峰顶的值为多少,cas为当前的谷底的值为多少
for(ll i=1;i<n;i++)
{
if(num[i]>=num[i-1])
{
ans+=M-cas;
M=cas=num[i];
}
else cas=num[i];
}
ans+=M-cas;
cout<<ans<<endl;
}
}
D题
规律总结,贪心
n个人站成一个环,每个人都会攻击自己左侧或者右侧的人。
有一个特别规定,如果某个人仅被1个人攻击,那么他攻击的人也必须是攻击他的那个人。而被0个人或者2个人攻击的人,攻击谁是没有限制的。
现在给定初始n个人的攻击方式,询问最少需要改变几个人的攻击方式,可以使得满足上述的特别规定。
观察样例总结一下,我们可以在稿纸上列一下基本的几种情况:
(此时不考虑成环,最右边的人的右侧不是第一个人)
L
R
LL
RR
LR
LRR
LLR
LLRR
这些都是满足特殊固定的情况。
容易证明其他情况全是不满足的。
那么我们总结一下上面的几种情况,实际上它包含了,连续字符不超过2个的所有情况。
我们可以由此把问题转化为,最少通过几次操作,可以使得原字符串中,不存在长度大于等于3的连续区域字符是相同的。
对于原字符串中,原本就存在两种字符的情况,我们直接for一遍扫出每一段字符相同的区域的长度,每长度为3的位置我们放一个与当前区域字符不同的字符即可构造出目标。因此直接对长度除3累加到答案上即可。
特别注意这个字符串是循环的,因此我们需要特别处理一下初始情况下,头和尾字符相等的情况。
另外,原字符串仅包含一种字符的时候,我们在任意一个位置插入一个不同的字符,那么原字符串就变成了长度 1和n-1的两个区域,最后答案就为最开始的1次操作加上(n-1)/3。
#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
int n;
cin>>n;
vector<char>S(n);
for(auto &x:S) cin>>x;
bool f=1;
for(int i=1;i<n;i++) if(S[i]!=S[i-1]) f=0;
if(f) cout<<1+(n-1)/3<<endl;
else
{
ll ans=0,cas=1;//cas代表当前扫到的连续的相同字符个数
while(S[0]==S[n-1]){cas++;n--;}//处理头和尾相同的情况
for(ll i=1;i<n;i++)
{
if(S[i]!=S[i-1])//如果当前位置与上一个位置不同,代表当前连续相同区域结束
{
ans+=cas/3;
cas=1;
}
else cas++;
}
cout<<ans+cas/3<<endl;
}
}
}
E题
交互,dp,观察数据,构造
交互题(说好的交互题就是二分呢???)
给你一个数值n(最大25),你需要构造一个n
×
\times
× n的矩阵,每个格子都为一个数值范围在[0,1e16]的值。
之后会给你k个值,为k条从(1,1)出发,只能向右或向下走,直到走到(n,n)的路径上所有数值的和。
要求你还原出这k条路径。
首先容易想到的,用26进制,第一行全部构造260=1,第二行全部构造261=26,第三行全部构造262=676…
用每一位对应的数字,代表第几行我们向右走了多少格。
但是这种方案是不可行的,因为他对我们构造的数值大小有限制,最大不能超过1e16,实际上我们自己写个简单程序检验一下,就能发现这种构造方案,我们需要的数值远远超过了longlong的上限。
那么接着想,我们肯定要构造一个矩阵,使得各个路径得到的值都不同,我们先不管怎么构造。我们先想想怎么从这个值还原出原路径,有一种想法是,记录每一行每个位置的前缀和,dfs暴力枚举我们在每行向右走了几步,但是实际上这种方案必然是会t的。因为我们可以写个简单的dp算出25
×
\times
× 25的矩阵的方案数,没记错应该是1e15级别的,这个复杂度dfs到世界末日。
那么我们实际上,通过dp数组已经发现了,方案数就已经很接近题目对我们给定的每个格子构造的数值大小限制上限了。并且这个值很大,我们必然是要通过一种O(1)或者O(logn)的方式,得到在每一个位置向右还是向下走的选择。
这里我想到的是一种,字典序的方案。我们从(1,1)走到(n,n),必然要走n-1次右,和n-1次下,我们把路径方向依次列出来,当做一个字符串,并且人为规定,字符’下‘是大于字符’右’的。
那么很容易得到,从(1,1)一直向右走到底,再向下走到底是数值最小的方案,我们对第一行和最后一列全部构造0,使得这条路径的值为0。
接下里是构造其他路径的过程:
对于任意一个位置(i,j),我们考虑从(1,1)走到(1,j)再走到(i,j)的路径,之后的路径没有限制,容易证明这种考虑方法是包含所有路径的。
对于我们从(1,1)走到(1,j)再走到(i,j)后,由于我们是按照上述字典序大小构造的,从(i,j)向右走的所有方案都是小于向下走的。
对于(i,j)这个位置来说,向右走的方案中,向右走到头再向下走到头是值最小的,向右走一格再向下走到头再向右走到头是值最大的。
我们从第n-1列向左构造,对每一列从上到下构造。
我们用一个x记录从(1,1)走到(1,j)再走到(i,j)后,向右走的最小值,即可通过上一段的路径计算出当前位置的值。(上方和右方都已经被构造了)。
而x的转移,从(i,j)位置转移到(i+1,j)位置,(这里想想上面的字典序,对位置(i,j),向右走的方案的值都是小与向下走的)增加的路径方案数,即为(i,j)向右走到(i,j+1)位置后,走到(n,n)的路径总数。这个我们可以通过预处理dp数组O(1)得到。
构造完毕后,我们可以通过dir数组,dir[i][j]记录从(i,j)出发,向右走一格,再任意走到(n,n)的最大值。(实际上这个路径是向右走一格,向下走到底,再向右走到底)
如果题目给我们的路径数值小于等于这个向右走的最大值,代表我们的方案被包含在向右走的情况里,反之则被包含在向下走到情况里。
由此,我们已经可以做到O(1)得到在每个位置的方向了。
#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
ll n;
ll sum;
ll field[30][30];//存储我们构建的矩阵每个位置的值,我们构造的矩阵的所有路线,对于field[i][j]来说,向下走的任意方案,一定比向右走的所有方案得到的值都更大
//以此来得到dir数组,做到O(1)指示我们的路径方向
ll dir[30][30];//dir[i][j]代表从当前位置向右走再到终点,可以获得的最大值,用来指示我们走到(i,j)时应该向下还是向右走
//实际上这个路线是先往后走一步,再向下走到底,再向右走到底
ll Data[30][30];//Data[i][j]代表从(i,j)走到(i,n)得到的值,就是一个反向的前缀和,辅助计算
ll dp[30][30];//dp[i][j]存储从(1,1)走到(i,j)有几种方案
void getdp()
{
dp[1][0]=1;
for(ll i=1;i<=25;i++)
{
for(ll j=1;j<=25;j++)
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
void getfield()
{
for(ll i=1;i<=n;i++) field[1][i]=field[i][n]=0;//构造第一行和最后一列为0
ll x=1;//x为对于(i,j)位置来说,从(1,1)先向右走到(1,j),再向下走到(i,j),再向右的最小值,即继续向右走到底再向下走到底
for(ll j=n-1;j;j--)
{
ll temp=0;//temp记录当前field[i][j]的上方所有值的和
for(ll i=2;i<=n;i++)
{
//见x代表的值
//第一行和最后一列我们构造的全为0,因此只需要让x减去field[i][j]上方和右方的累加值即为field[i][j]应当构造的值
field[i][j]=x-temp-Data[i][j+1];
temp+=field[i][j];//累加竖直上方的前缀和
Data[i][j]=field[i][j]+Data[i][j+1];//累加右方的前缀和
x+=dp[n-i+1][n-j];//对于field[i][j]来说,它第一步向右走,再走到终点的情况即为[n-i+1,n-j]的矩阵的方案数
//那么对于field[i+1][j]来说,它向右走的最小值就应该比field[i][j]的最小值加上对应的dp值方案数
}
}
}
void getdir()
{
for(ll i=1;i<=n;i++)
{
for(ll j=1;j<n;j++)
{
for(ll k=i;k<=n;k++)
dir[i][j]+=field[k][j+1];
for(ll k=j+2;k<=n;k++)
dir[i][j]+=field[n][k];
dir[i][j]+=field[i][j];
}
}
}
int32_t main()
{
IOS;
cin>>n;
getdp();
getfield();
getdir();
for(ll i=1;i<=n;i++)
{
for(ll j=1;j<=n;j++)
cout<<field[i][j]<<' ';
cout<<endl;
}
ll k;
cin>>k;
while(k--)
{
cin>>sum;
ll nowr=1,nowc=1;//nowr为我们当前所在行,nowc为我们当前所在列
while(nowr<=n&&nowc<=n)
{
cout<<nowr<<' '<<nowc<<endl;
if(dir[nowr][nowc]<sum||nowc==n) {sum-=field[nowr][nowc];nowr++;}
//如果我们当前的值,比向右走得到的最大值都大的话,代表当前方案应该在下方
else {sum-=field[nowr][nowc];nowc++;}
//如果我们当前的值,比向右走得到的最大值要小或等于,代表当前方案应该在右侧
}
cout<<endl;
}
}