Codeforces Round #663 (Div. 2)A-D题解
//写于大号rating值2184/2184,小号rating值1887/1887
//大号打星,本场小号打的,排名890,rating值+6
//本场19分钟切完ABC后,分析出了D的第一层结论后,脑抽得认为剩余情况最多是个3 × \times × 3的矩阵
//暴力枚举了一波白给了一发re
//发现自己想错了重构一遍代码后回归了正确的dp思路,但是此时心态开始变得急躁了起来
//对输入数据的预处理做得非常糟糕,强行分成四种情况if…else分类讨论,造成代码量相对巨大
//状态转移的方程存在四种,出错后面对冗长的代码心态变得更加急躁
//最后的结果就是花了101分钟也没能把这个拥有正确思路的题写出来
//反思总结一下,我的问题在于两点
//1.心态。
//最近的比赛写得一直过于顺利,依靠快手速上分,出错重构整个题目的代码后自己的心理素质没能跟上
//写题不顺的逆风时间如何合理把握和调整自己的心态还需要更多的锻炼
//2.代码风格。
//过于相信自己分类讨论的能力,在心态烦躁的情况下更加加重了自己过分相信自己结论的缺点
//不愿意继续总结和优化结论对数据和算法操作进行优化,导致码量增加难以维护,又加剧了心态的恶化
//每次错误和失败,都是在为明天更好的自己打下基础
比赛链接:https://codeforces.com/contest/1391
A题
过题用时:4min
位运算弱智题
读错题意看成了异或运算,多花了些时间。
题意为对于一个长度为n的排列,对于任意的区间需要满足这个区间内所有数字的或运算结果不小于这个区间的长度。
首先或运算的结果是不会比参与运算的两个数值小的
其次这是一个排列,你取任意的一个长度为L的区间,这个区间必定有一个数字的值是大于等于L的
没了…任意顺序输出这个排列都能过。
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
int main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n;
cin>>n;
for(ll i=1;i<=n;i++) cout<<i<<' ';
cout<<endl;
}
}
B题
过题用时:5min
观察总结
给定一个n × \times ×m区域,每个区域只有‘D’和‘R’代表向下和向右两个方向。
现在询问你最少修改几个地块的方向,可以使得从这个区域的任意一个地块出发,都能到达(n,m)点
首先观察最后一行,由于我们只能向下和向右走又必须能到(n,m)点,因此这一行除了(n,m)点外方向必须全为R
之后讨论上方的其他行,每一行的最后一个位置(i,m)同样由于只能向下和向右又必须能到(n,m)点的原因,这些位置的方向必须全为D
其他剩余的地块,无论如何构造,由于方向只能向下或者向右,最终都会走到最后一行或者最后一列,而经过上面的构造,最后一行和最后一列都是能走到(n,m)的,因此剩余区域不需要做任何改变。
计算下最后一行除了(n,m)点外,不是R的地块数量
计算下最后一列除了(n,m)点外,不是D的地块数量
两者加起来即可
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
string s[107];
int32_t main()
{
IOS;
int t;
cin>>t;
while(t--)
{
ll n,m;
cin>>n>>m;
for(ll i=0;i<n;i++) cin>>s[i];
ll ans=0;
for(ll i=0;i+1<n;i++)
if(s[i][m-1]!='D') ans++;
for(ll i=0;i+1<m;i++)
if(s[n-1][i]!='R') ans++;
cout<<ans<<endl;
}
}
C题
过题用时:10min
规律总结,构造
要求我们构造一个排列,使得按照以下两条规则建无向图后,存在闭环。
1.对于任意的下标i满足1<=i<=n,找到最大的下标j满足1<=j 2.对于任意的下标i满足1<=i<=n,找到最小的下标j满足i
首先我们需要想一下,什么时候会构造出闭环呢。
从三个数字进行讨论,我们可以注意到:
排列:3 1 2
排列:2 1 3
下标:1 2 3
这两个排列,下标2对应的数字是最小的1,下标(1,2)和(2,3)都会建边。
而对于下标1和下标3来说,不论这两个下标对应的数值谁大谁小,(1,3)都会建边。
可以发现此时便已经构成了一个闭环。
从这个结论可以推广出,如果我们的排列存在某个位置的值既小于左侧又小于右侧(按照数值大小建立波形图的话也就是存在波谷),那么必然是存在闭环的。
那么不满足上述条件的排列又是哪些,这些排列又是否会构成闭环呢?
不存在波谷的排列,也就意味着只存在一个波峰
也就是说对于一个长度为n的排列来说,存在一个下标i满足:
1.对于下标i左侧的区域,从左往右是单调递增的
2.对于下标i右侧的区域,从左往右是单调递减的
对于这样的数列,容易得到,整个图只有下标i与其他n-1个下标构成的n-1条边,构成了一个树结构,不存在闭环。
由此我们只需要计算出总共的排列种数,减去不存在波谷的排列数即可。
总共的排列数就是n!
而不存在波谷的排列数,我们的构造方法可以认为是按照数值从小到大1-n的顺序依次往n个位置摆,由于我们不能出现波谷,因此我们每次只能放在当前可放区域的最左侧或者最右侧,每次有两个选择方案(除了放最后一个数字n的时候,只有一个位置了),方案数为2^(n-1)
以排列n=5为例
一开始五个空位:… … … … … …
我们开始摆1,可以摆在最左侧也可摆在最右侧
我们摆在最左侧:1 … … … …
继续摆2,可以摆在最左侧也可以摆在最右侧
我们摆在最右侧:1 … … … 2
继续摆3,可以摆在最左侧形成:1 3 … … 2也可摆在最右侧形成:1 … … 3 2
继续摆放剩下最后的5时,随意举例:1 3 … 4 2
此时我们的5只有一种选择方案
计算n!-2^(n-1)即可
#include
#define ll long long
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll mod=1e9+7;
int32_t main()
{
IOS;
ll n;
cin>>n;
ll ans=1,rest=1;//ans计算n!,rest计算2^(n-1)
for(ll i=2;i<=n;i++)
{
ans=ans*i%mod;
rest=rest*2%mod;
}
cout<<(ans-rest+mod)%mod<<endl;
}
D题
模型转化,dp
//比赛时白给了一发后,马上回归到了正确的想法,但是预处理太差,分类四种情况写了个将近300行的怪物,写反dp数组的下标wa后越修改心态越炸
//保持心态和优秀的预处理都很重要
首先我们需要推出什么情况下是无法构造的。
当我们存在4 × \times × 4的子矩阵时,注意到左上右上左下右下分别有四个区域互不重叠的2 × \times × 2矩阵,这些矩阵的1的个数均为奇数,那么我们当前的4 × \times × 4的子矩阵包含的1的个数为这四个2 × \times × 2矩阵的和,必然是一个偶数,与要求相悖。
由此得到,当存在4 × \times × 4的子矩阵时无法构造,直接输出-1,也就是n>3且m>3的情况
对于可以构造的情况,我们预处理一下,把当前矩阵转置成为n
容易得到,此时n只有1,2,3三种情况。
也就是说对应的我们只有两行的2 × \times × 2矩阵,我们把这些矩阵包含的1的个数统计出来,按照是否需要改变分类为0和1两种情况,需要改变的矩阵是1,不需要改变的矩阵是0。
对于每一列,最多有两个矩阵,我们用二进制来表示,第一行是二进制的第一位,第二行是二进制的第二位:
0=00对应这一列的两个2 × \times × 2矩阵都不需要被改变
1=01对应这一列的第一行的2 × \times × 2矩阵需要被改变
2=10对应这一列的第二行的2 × \times × 2矩阵需要被改变
3=11对应这一列的一二行的2 × \times × 2矩阵均需要被改变
把每一列的状态存在num[]数组里,我们的目的是算出把整个num数组变为0的最小操作次数
而对于n × \times ×m的区域,
改变某一个数字,会使得包含它的矩阵状态从1变为0,或者从0变为1(对应异或运算)
我们如果改变第一列和最后一列的数字,那么只会对第一列的矩阵或最后一列的矩阵产生影响,
而对于改变中间区域的数字,不仅会对那个数字对应列的2 × \times × 2矩阵产生影响,也会对前一列的2 × \times × 2矩阵产生影响
而造成的影响,我们可以通过异或运算来完成。结合代码dp转移过程看吧。
#include
#define ll long long
#define llINF 9223372036854775807
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const ll maxn=1e6+7;
string s[maxn];
ll n,m;
ll num[maxn];//num[i]的二进制代表在第i列对应的2*2矩阵是否需要进行更改,10代表第二层需要改变,01代表第一层需要改变
ll dp[maxn][4];//dp[i][j]代表对于第i列,我们把1到i-1列全部更新为满足条件后,第i列情况为j需要的最少次数
bool field[3][maxn];
int32_t main()
{
IOS;
cin>>n>>m;
for(ll i=0;i<n;i++)
cin>>s[i];
if(n>3&&m>3) cout<<-1<<endl;
else
{
if(n>m)//预处理,转化成数字方便后续代码,且转置为n
{
for(ll i=0;i<m;i++)
for(ll j=0;j<n;j++)
field[i][j]=s[j][i]-'0';
swap(n,m);
}
else
{
for(ll i=0;i<n;i++)
for(ll j=0;j<m;j++)
field[i][j]=s[i][j]-'0';
}
for(ll i=0;i+1<m;i++)
{
for(ll j=0;j+1<n;j++)
{
ll temp=0;
if(field[j][i]) temp++;
if(field[j][i+1]) temp++;
if(field[j+1][i]) temp++;
if(field[j+1][i+1]) temp++;
if(temp%2==0) num[i+1]+=(1<<j);//注意这里下标变为i+1了,从1开始方便后面dp
}
}
ll lim=1<<(n-1);
dp[0][0]=dp[0][1]=dp[0][2]=dp[0][3]=0;
for(ll i=1;i<m;i++)
{
for(ll j=0;j<lim;j++)
{
if(j) dp[i][j^num[i]]=dp[i-1][j]+1;//每一列改变时,不仅会改变当前列的2*2矩阵,也会改变前一列的2*2矩阵
else dp[i][0^num[i]]=dp[i-1][0];//不改变当前列数字的情况,不必+1
}
}
ll ans=llINF;
for(ll i=1;i<lim;i++) ans=min(ans,dp[m-1][i]+1);//最后一列我们还可以单独进行改变操作
ans=min(ans,dp[m-1][0]);
cout<<ans<<endl;
}
}