Codeforces round 654 赛后解题报告
先吐槽一下怎么 A-D 都是结论题啊啊
A. Magical Sticks
我们可以先确定下来,我们一定只对于未进行过拼接的木棍拼接。
学过等差数列的朋友们,对于 时都有一个常识:(不会有人没学过等差数列吧)
即对于任意满足 的 。都有:
所以答案为 。
如果 的话,我们可以把问题转化为 。因为这些木棍拼接起来长度都为 。答案为 。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
int n;
signed main() {
int t;
t=read();
while(t--) {
n=read();
if(n&1) {
cout<<(n-1)/2+1<<endl;
}
else {
cout<<n/2<<endl;
}
}
return 0;
}
B. Magical Calendar
我们令 为一个星期的天数()。
我们可以把问题转化为当 和 两种情况。
当 时,连续 天一定会出现在至少两个星期里。我们会发现以第一行的任意一个位置作为起点都会有一个不同的形状,且一定相连。因为此时 。所以一周里天数为 时的答案就是 ,在最后计算答案时可以用等差数列求和公式解决。
当 时,无论如何不能跨越星期,所以至多一种情况,就是 天排列在一个星期里。 加一即可。
#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
int n,r;
signed main() {
int t;
t=read();
while(t--) {
n=read();r=read();
int ans=0;
if(n<=r) {
ans+=(n-1)*n/2;//等差数列求和,n<=k
ans+=1;//n>k
}
else {
ans+=(r+1)*r/2;//等差数列,注意上下比阿奴街
}
cout<<ans<<endl;
}
return 0;
}
C. A Cookie for You
这个题还是推理题
首先我们可以想到,第二类人(以下简称 )做出任何选择不会印象第一类人(以下简称 )的选择。因为 选择数量少的, 选择数量多的。所以我们的考虑的主体一定是 。
那么首先我们可以先判断,如果饼干总数比人数少,一定是 No。然后我们再继续。
我们可以考虑如果数量较少的那一类饼干数量大于等于 的人数(听起来怪怪的),那么一定 Yes。因为此时 。 解决完了,一定应付的过来 。
如果数量较少的那一类饼干数量小于等于 的人数,我们要考虑让 去配合 。但是我们可以发现是不可行的。因为为了让 去吃原来数量多的那一类,就一定要让 把原来多的吃掉很多,知道比原来较少的还要少。这个局面达不到得到, 的人数够不够多不说,我们可以发现此时的局面还不如一开始,因为现在较少的那一类饼干的数量比之前较少的那一类饼干的数量还要少。所以一定是 No。
由于 YES,NO 的大小写随意,程序就随意啦
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
int n,a,b,m;
signed main() {
int t=read();
while(t--) {
a=read();b=read();n=read();m=read();
if(a+b<n+m) {
printf("No\n");
}
else if(min(a,b)>=m) {
printf("Yes\n");
}
else {
printf("nO\n");
}
}
return 0;
}
D. Grid-00100
我们先来证明 。用平方差公式展开:,。得证。
我们先可以对于:
这个式子进行一些思考,根据 的性质,我们可以知道我们要让 尽量相近, 尽量相近。我们就可以用一个类似于“抽屉原理”的思路来解决本题。
我们可以先把矩阵全部用 填上,在一点一点把多余的 扣掉。
我们在扣 的时候。可以考虑选择行,列各不同的 进行一次去除。如果我们还需去除的不足 个,就选择剩下的 个去掉即可,此时答案为 。如果正好 ,那么答案为 。
最后提醒:别输出空格。别像笔者一样傻乎乎的qwq。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
const int maxn=310;
int n,a[maxn][maxn],k;
signed main() {
int t=read();
while(t--) {
n=read();k=read();
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
a[i][j]=1;
}//全填成1
}
int sum=n*n,ans,cnt=0;
if((sum-k)%n==0) {
ans=0;
}//计算答案,要么0,要么2
else {
int m=(sum-k)/n;
ans=2;
}
while(sum>k) {
if(sum-k>n){//去掉n个
sum-=n;
for(int i=1;i<=n;i++) {
a[i][(i+cnt)%n+1]=0;
}
}
else {//去掉k个
for(int i=1;i<=sum-k;i++) {
a[i][(i+cnt)%n+1]=0;
}
sum=k;
}
cnt++;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
cout<<a[i][j];//输出,注意没有空格
}
cout<<endl;
}
}
return 0;
}
我们模拟一下一组输入:
3 5
那么我们就会得到如下的变化过程:
应该就可以理解算法的精髓了吧~~
E1. Asterism (Easy Version)
首先我们可以确定是一个 易得
1.
2.
因此我们只需要考虑第三种情况。由于时间充裕,我们可以枚举
我们采取计算每一位上有多少个可以放的怪兽,因为可以放在
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
const int maxn=2e4+10;
int n,a[maxn],p,t[maxn<<1];
vector<int> ans;
signed main() {
n=read();p=read();
for(int i=1;i<=n;i++) {
a[i]=read();
t[a[i]]++;
}
sort(a+1,a+n+1);
for(int i=1;i<=a[n]+n;i++) {
t[i]=t[i]+t[i-1];
}
int k=max(0LL,a[n]-n+1);
for(int x=k;x<=a[n];x++) {
bool flag=1;
for(int i=x;i<x+n-1;i++) {
if((t[i]-(i-x))%p==0) {
flag=0;
break;
}
}
if(flag) {
ans.push_back(x);
}
}
cout<<ans.size()<<endl;
int sz=ans.size();
for(int i=0;i<sz;i++) {
cout<<ans[i]<<" ";
}
return 0;
}
E2. Asterism (Hard Version)
我们下面的推导和讲解需要用到本题 Easy Version 中的结论,可以参考我的 题解。
我们知道
变形得到:
所以一旦
顺便提一下怎么处理
3 2
1000000000 1 999999999
排好序后是
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
const int maxn=1e5+10;
int n,ex[maxn<<1],a[maxn],p;
map<int,int> t;
vector<int> ans;
int mod(int x,int p) {
return (x+p)%p;//注意i-ti可能小于 0
}
signed main() {
n=read();p=read();
for(int i=1;i<=n;i++) {
a[i]=read();
}
sort(a+1,a+n+1);
a[n+1]=a[n]+1;
for(int i=a[n]-n+1;i<=a[n];i++) {
int q=upper_bound(a+1,a+n+1,i)-a-1;
ex[mod(i-q,p)]++;
}
int k=max(0LL,a[n]-n+1);
for(int x=k;x<=a[n];x++) {
if(!ex[x%p]) {
ans.push_back(x);
}
ex[mod(x-(upper_bound(a+1,a+n+1,x)-a-1),p)]--;
ex[mod(x+n-(upper_bound(a+1,a+n+1,x+n)-a-1),p)]++;
}
cout<<ans.size()<<endl;
int sz=ans.size();
for(int i=0;i<sz;i++) {
cout<<ans[i]<<" ";
}
return 0;
}
F. Raging Thunder
思路10min,程序2h,调试7h。
看到上面的字,没有深厚代码功底和强大毅力的同学建议不要尝试切这道题。
忠告:建议大家别看代码,因为太乱,基本上看不懂,看看思路和下面的细节提醒即可,代码当且仅当是在不知道哪里错是,对拍一下,对拍程序下面会给
我们回到正题,看到这个题。我们发现所有满足 >>><<<
,>>>>
,<<<<
形式的区间会调到一个地方,所以我们要重点关注这个结构,下面我们称之为 “小可爱”(因为它像>_<这个表情):D。我们给三种“小可爱”起个代号吧(也就是他们的类型):
“小可爱”形状 | 编号 |
---|---|
>>> | |
<<< | |
><< |
看到区间的维护,很容易想到线段树,但是怎么维护?我们只需要维护 即可,见下表:
全称 | 意义 | |
---|---|---|
左节点编号 | ||
右节点编号 | ||
从左起最长的“小可爱”的长度 | ||
从右起最长的“小可爱”的长度 | ||
从左起最长的“小可爱”的编号 | ||
从右起最长的“小可爱”的编号 | ||
懒标记 |
线段树的定义
struct seg_tree {
int ln,rn,mn,l,r;
short lt,rt;
int lazy;
int ans() {
return max(max(ln,rn),mn);
}
int len() {
return r-l+1;
}
void make() {
ln=rn=mn=lt=rt=l=r=lazy=0;
}
bool operator == (const seg_tree x) {
return ln==x.ln&&rn==x.rn&&mn==x.mn&<==x.lt&&rt==x.rt&&l==x.l&&r==x.r;
}
}s[2][maxn*20],ret[maxn*20];
知道两个区间的这些信息,我们就可以合并出一个新的区间,我们也就可以可以解决线段树的 build
。这个讨论太复杂了,讲不太清楚,可以自己思考一下 :)。
合并:
seg_tree pushup(seg_tree a,seg_tree b) {
calcnum++;
seg_tree ret;
ret.make();
if(a==ret) {
return b;
}
if(b==ret) {
return a;
}
//l:
//1:>>>>
//2:<<<<
//3:>><<
if(a.len()==a.ln) {
if(a.lt==1) {
ret.ln=a.ln+b.ln;
if(b.lt==1) {
ret.lt=1;
}
else {
ret.lt=3;
}
}
else {
if(b.lt==2) {
ret.lt=a.lt;
ret.ln=b.ln+a.ln;
}
else {
ret.ln=a.ln;
ret.lt=a.lt;
}
}
}
else {
ret.ln=a.ln;
ret.lt=a.lt;
}
//r:
//1:>>>>
//2:<<<<
//3:>><<
if(b.len()==b.rn) {
if(b.rt==2) {
ret.rn=a.rn+b.rn;
if(a.rt==2) {
ret.rt=2;
}
else {
ret.rt=3;
}
}
else {
if(a.rt==1) {
ret.rt=b.rt;
ret.rn=b.rn+a.rn;
}
else {
ret.rn=b.rn;
ret.rt=b.rt;
}
}
}
else {
ret.rn=b.rn;
ret.rt=b.rt;
}
//m:
//1:>>>>
//2:<<<<
//3:>><<
if(a.rt==1) {
ret.mn=a.rn+b.ln;
}
else if(a.rt==2) {
if(b.lt==2) {
ret.mn=a.rn+b.ln;
}
}
else {
if(b.lt==2) {
ret.mn=a.rn+b.ln;
}
}
//
if(ret.mn<a.mn) {
ret.mn=a.mn;
}
if(ret.mn<b.mn) {
ret.mn=b.mn;
}
ret.l=a.l;ret.r=b.r;
return ret;
}
build
void build(int l,int r,int p) {
calcnum++;
if(l==r) {
s[0][p].ln=s[0][p].rn=s[0][p].mn=1;
s[0][p].l=s[0][p].r=l;
s[0][p].lazy=0;
if(a[0][l]==0) {//'>' 对应 0
s[0][p].lt=s[0][p].rt=1;
}
else {//'<' 对应 1
s[0][p].lt=s[0][p].rt=2;
}//初始化,也要讨论qwq
s[1][p].ln=s[1][p].rn=s[1][p].mn=1;
s[1][p].l=s[1][p].r=l;
s[1][p].lazy=1;
if(a[1][l]==0) {
s[1][p].lt=s[1][p].rt=1;
}
else {
s[1][p].lt=s[1][p].rt=2;
}
return ;
}
int mid=(l+r)>>1;
build(l,mid,p<<1);
build(mid+1,r,p<<1|1);
s[0][p]=pushup(s[0][p<<1],s[0][p<<1|1]);
s[1][p]=pushup(s[1][p<<1],s[1][p<<1|1]);
}
那么有的小伙伴会发现我的线段树定义了s[2][maxn]
。这是因为我们的特殊修改操作。我们发现,我们要处理这个修改操作最好的办法就是要建两颗树,一颗是原来的,一颗是倒过来的。只要涉及到翻转,就交换一下信息。所以我们要维护两颗树。注意懒标记。
modify
void modify(int l,int r,int p,int x,int y) {
calcnum++;
pushdown(p);
if(y<l||r<x) {
return ;
}
if(x<=l&&r<=y) {
swap(s[0][p],s[1][p]);
s[0][p].lazy^=1;
return ;
}
int mid=(l+r)>>1;
modify(l,mid,p<<1,x,y);
modify(mid+1,r,p<<1|1,x,y);
s[0][p]=pushup(s[0][p<<1],s[0][p<<1|1]);
s[1][p]=pushup(s[1][p<<1],s[1][p<<1|1]);
}
最后是query
。我们可以直接写一个返回类型为 seg_tree 的函数,当然这里提一下 void。我们可以在开一个数组放 答案,因为不会一个完整区间里数都是答案需要的。比如说对于 ret
数组维护的就是 ret[1]
就是最后答案区间啦。
query
void query(int l,int r,int p,int x,int y) {
pushdown(p);
if(y<l||r<x) {
seg_tree qwq;
qwq.make();
ret[p]=qwq;
return ;
}
if(x<=l&&r<=y) {
ret[p]=s[0][p];
return ;
}
int mid=(l+r)>>1;
query(l,mid,p<<1,x,y);
query(mid+1,r,p<<1|1,x,y);
ret[p]=pushup(ret[p<<1],ret[p<<1|1]);
return ;
}
最后说一下细节问题,这个题调试7h就是因为细节太多 :(
1.懒标记我们一定要小心,因为如果一个区间的懒标记为
2.注意
3.在 pushup
里面一定要小心小心再小心,一不留神就会写错。
4.建议多对拍
5.对于调试,有一个比较好的方法,特别是复杂度不对时(因为我遇到了),可以屏蔽一个函数,看看耗时是否减小很多,如果是的,那么这个函数一定出大问题了
对拍程序见下
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
int n,q;
signed main() {
freopen("F.in","w",stdout);
srand(time(NULL));
n=400000+rand()+rand()+rand();
q=90000+rand();
cout<<n<<" "<<q<<endl;
for(int i=1;i<=n;i++) {
printf("%c",rand()%2==0? '>':'<');
}
cout<<endl;
for(int i=1;i<=q;i++) {
int x,y;
x=35000+rand();
y=rand()+x+rand();
printf("%lld %lld\n",x,y);
}
return 0;
}
//造数据
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
signed main() {
int t=read();
system("cls");
for(int i=1;i<=t;i++) {
double a,b;
printf("Test Case %d\n",i);
printf("I'm makeing datum :D\n");
system("duipai.exe");
printf("I'm working on the answer ^_^\n");
a=time(NULL);
system("F_std.exe");
b=time(NULL);
printf("Done in %lf s\n",b-a);
printf("Running.......\n");
a=time(NULL);
system("F.exe");
b=time(NULL);
printf("Done in %lf s\n",b-a);
if(!system("fc my.out std.out")) {
cout<<"Accepted !!!\n\n";
}
else {
cout<<"Wrong answer>_<\n\n";
}
}
//fclose(stdin);
//fclose(stdout);
return 0;
}
//测试
完整程序在这里
不要在意火车头 :D
共 2 条回复
写的很棒 继续保持
点赞!