[总结] Codeforces Round #724 (Div. 2)

[总结] Codeforces Round #724 (Div. 2)

A. Omkar and Bad Story

传送门

本题有两种解法。

  1. 暴力模拟,借用 \(queue\)

代码为题解代码。@Jr_zLiwen

#include<bits/stdc++.h>
#define rep(a,b,c) for(int c=(a);c<=(b);++c)
#define Clear(a) memset(a,0,sizeof(a))
using namespace std;
inline int read()
{
	int res=0;char ch=getchar();bool flag=0;
	while(ch<'0'||ch>'9')flag=(ch=='-'),ch=getchar();
	while(ch<='9'&&ch>='0')res=res*10+(ch^48),ch=getchar();
	return flag ? -res : res;
}
map<int,bool> t;
const int N=105;
int a[N*5],dd[N*5+4],p;int que[100005],H=1,T,qq;
inline void Solve()
{
	Clear(a);H=1;qq=T=0;Clear(dd);t.erase(t.begin(),t.end());
	int n=read();rep(1,n,i)t[(a[i]=read())]=1;p=n;//rep(1,n,i)printf("%d ",a[i]);
	rep(1,n,i)rep(1,n,j)if(i!=j)if(!t[abs(a[i]-a[j])]){t[(que[++T]=abs(a[i]-a[j]))]=1;}
	while(H<=T&&p<=300)
	{
		a[++p]=que[H++];//rep(H,T,j)printf("%d ",que[j]);puts("");
		rep(1,p-1,i)if(!t[abs(a[p]-a[i])])que[++T]=abs(a[p]-a[i]),t[abs(a[p]-a[i])]=1;
	}
	if(H>T){puts("Yes");printf("%d\n",p);rep(1,p,i)printf("%d ",a[i]);puts("");return;}puts("No");
}
int main(){int T=read();while(T--)Solve();}
  1. 如果有负数就直接输出 \(No\),否则输出 \([0,100]\) 所有数。

代码来自@一九年

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define N 101000
using namespace std;

int n,a[N],T,flag,k,b[N],flagg;
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		flag=0;k=0;flagg=0;
		for(int i=1;i<=n;i++){
			scanf("%d",&a[i]);
			if(a[i]<0) flag=1;
			if(a[i]==0) flagg=1;
		}
		sort(a+1,a+n+1);
		if(flag==1){
			cout<<"NO"<<endl;
			continue;
		}
		else{
			cout<<"YES"<<endl;
			cout<<101<<endl;
			for(int i=0;i<=100;i++) cout<<i<<" ";
			cout<<endl; 		
		}
	}
	return 0;
}

B. Prinzessin der Verurteilung

传送门

只需要暴力往外扩展就行,类似于 \(BFS\) 求边权为 \(1\) 的最短路的过程,保证先扩展到的满足条件的字符串一定是答案。

这里声明一个用法:

if(a.find(s)==string::npos)

这并不是 \(C++11\) 的东西,可以检查一个字符串是否在另一个字符串内,返回指针

\(string\) 的东西要用 \(string\) 库函数来解决。

比如往 \(string\) 后插东西,在直接引用位置的情况下需要 \(resize\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#include <string>
template <typename T>
inline T read(){
	T x=0;char ch=getchar();bool fl=false;
	while(!isdigit(ch)){if(ch=='-')fl=true;ch=getchar();}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
	return fl?-x:x;
}
#include <queue>
#define Pair pair<string,int>
#define read() read<int>()
#define mp make_pair
int n;
string a,b;
void solve(){
	queue <Pair> q;
	q.push(mp(b,0));
	while(1){
		Pair p=q.front();q.pop();
		string s=p.first;int pos=p.second;
		s.resize(pos+1);
		for(int i=0;i<26;i++){
			s[pos]=(char)('a'+i);
			//cerr<<s[pos]<<endl;//
			if(a.find(s)==string::npos){
				cout<<s;puts("");return ;
			}
			q.push(mp(s,pos+1));
		}
	}
}
int main(){
	int T=read();
	while(T--){
		cin>>n>>a;
		solve();
	}
}

代码参考@Acc_Robin


C. Diluc and Kaeya

传送门

应该说是唯一独立想出来的题了。

可以参考我的题解

题解

  • 何为无解情况?

显然,当 \(n(D)\)\(n(K)\) 互质时无解,因为此时第一次出现 \(n(D):n(K)\) 这个比例,它肯定是无法划分的。

  • 如何统计答案?

由于第一次出现比例 \(c\) 时是无解情况,第二次出现时一定存在一种可划分方案把 \(S\) 划分为比例相同的两部分。

一边扫描一边用 \(map\) 统计即可,复杂度 \(\text O(n\ logn)\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#include <string>
template <typename T>
inline T read(){
    T x=0;char ch=getchar();bool fl=false;
    while(!isdigit(ch)){if(ch=='-')fl=true;ch=getchar();}
    while(isdigit(ch)){
        x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
    }
    return fl?-x:x;
}
#include <map>
#define Pair pair<int,int>
const int maxn = 5e5 + 10;
int ans[maxn],top=0;
#define read() read<int>()
#define mp make_pair
int n;
char ch[maxn];
int gcd(int a,int b){
    if(!b)return a;
    return gcd(b,a%b);
}
void solve(){
    memset(ans,0,sizeof ans);
    map <Pair,int> m;
    int D=0,K=0;
    for(int i=1;i<=n;i++){
        D+= ch[i]=='D';K+= ch[i]=='K';
        int g=gcd(D,K);
        //cerr<<D<<" "<<K<<" "<<g<<endl;//
        ans[i]=++m[mp(D/g,K/g)];
    }
    for(int i=1;i<=n;i++)printf("%d ",ans[i]);
    puts("");
}
int main(){
    int T=read();
    while(T--){
        n=read();cin>>(ch+1);
        solve();
    }
}

D. Omkar and Medians

传送门

考虑到中位数一个非常重要的性质:向原序列加入两个数之后中位数最多移动 1 位。

所以用 \(multiset\) 维护前驱后继(你写平衡树也没人拦着你)。

假设现在在 \(b_{i+1}\) 这个数,原序列 \([a_{1},a_{2i-1}]\) 的中位数是 \(b_i\),当前序列在此数有没有解 当且仅当当前数是\([b_1,b_i]\) 的前驱、后继或者在 \([b_1,b_i]\) 中出现过。

思路来自@neal

强调一个东西:

*prev(it);//it是迭代器

这是 \(C++11\) 里的东西,可以返回前驱。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
template <typename T>
inline T read(){
	T x=0;char ch=getchar();bool fl=false;
	while(!isdigit(ch)){
		if(ch=='-')fl=true;ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
	return fl?-x:x;
}
#include <set>
#define read() read<int>()
const int maxn = 2e5 + 10;
int a[maxn],n,la;
multiset <int> s;
bool check(int x){
	if(!s.size())return true;
	for(int i=0;i<2;i++){
		multiset<int> :: iterator it=(i==0)?s.lower_bound(x):s.upper_bound(x);
		if(it!=s.end() && *it==la)return true;
		if(it!=s.begin() && *prev(it)==la)return true;
	}
	return false;
}
void solve(){
	s.clear();la=0;
	for(int i=1;i<=n;i++){
		if(!check(a[i])){
			puts("NO");return ;
		}
		la=a[i];s.insert(a[i]);
	}
	puts("YES");
}
int main(){
	int T=read();
	while(T--){
		n=read();
		for(int i=1;i<=n;i++)a[i]=read();
		solve();
	}
}

E. Omkar and Forest

传送门

题意

给你一个 \(n\times m\) 的网络,其中 \(0\) 代表数字,'#' 可以填任意一个非负整数。

问你有多少种填的方案。

其中需要满足:

  • 任意相邻两个点之间差的绝对值不超过 \(1\)
  • 对于 '#' 点来说,该点必须严格大于至少一个相邻的点。

题解

考虑本题一个比较难想的性质:

  • 当图中的 '#' 一部分确定为 \(0\) 时,此时图的解是唯一的。

这是比较难推的,但是貌似举不出反例来。

比如当前点为正权值的 '#',周围的权值集合为 \(S\)

  • \(S=1\),权值确定。
  • \(S\not=1\),由于第一条性质,'#' 的权值也是唯一的。

所以合理外推,不难得到对于确定的 '#' 为 \(0\) 的布局,方案是一定的。

\(ans=2^{cnt}\),其中 \(cnt\)\(0\) 的个数。

因为当布局中 \(0\) 不存在时,不存在合法方案,因此如果网络全是 '#' 答案需要 \(ans--\)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
template <typename T>
inline T read(){
	T x=0;char ch=getchar();bool fl=false;
	while(!isdigit(ch)){
		if(ch=='-')fl=true;ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
	return fl?-x:x;
}
const int maxn = 2000 + 10;
#define read() read<int>()
#define LL long long
const LL P = 1e9 + 7;
char ch[maxn][maxn];
int main(){
	int T=read();
	while(T--){
		int n=read(),m=read();bool fl=false;
		for(int i=1;i<=n;i++)cin>>(ch[i]+1);
		LL ans=1;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++){
				if(ch[i][j]=='0')fl=true;
				else (ans*=2LL)%=P;
			}
		}
		if(!fl)ans=(ans-1+P)%P;
		printf("%lld\n",(ans+P)%P);
	}
	return 0;
}

后记

个人感觉本题收获还是比较大的。

  • 遇到难想的题抓住题目性质(也许太宽泛了)。
  • 抓住部分结构然后合理外推是常见套路,比如:
    • 微扰法。
    • 各种 \(DP\) 的状态设计和转移分析。
    • \(\cdots\)

这些都是需要把握局部的性质然后合理外推

但是有些题比如组合数学也许就要把握整体了。(下一道)


F. Omkar and Akmar

传送门

首先从宏观角度分析:如何才能出现终止状态?

显然是 \(A\)\(B\) 夹着一个空白位置的时候。(局部分析)

所以合理外推,对于终止状态,不难发现:

  • 后手一定赢,也就是说 \(A\)\(B\) 一定是成对出现的。
  • 不可能出现两个及以上个空位置相连的情况。

有了这两个性质,公式就好写了,可以枚举空位置的个数 \(i\)

  • \(ans=n\sum_{i=n\ mod\ 2}^{\lfloor{n/2}\rfloor}2*C(n-i,i)*(n-i-1)!\)

其中 \(C(n-i,i)\) 为插板法的方案数,\((n-i-1)!\) 是板子的圆排列个数。

每插一次板子等价于填了一个 \(A\) 和一个 \(B\),因为 \(AB\)\(BA\) 是等价的,所以需要乘以 \(2\)

由于这个形成的圆盘转动后并不是原来的方案了,所以需要乘以 \(n\)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
template <typename T>
inline T read(){
	T x=0;char ch=getchar();bool fl=false;
	while(!isdigit(ch)){
		if(ch=='-')fl=true;ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
	return fl?-x:x;
}
#define LL long long
const int maxn = 1e6 + 10;
const LL P = 1000000007LL;
LL fac[maxn],invfac[maxn];
int n;
LL power(LL a,LL b){
	LL res=1;
	while(b){
		if(b&1)res=1LL*res*a%P;
		a=1LL*a*a%P;
		b>>=1;
	}
	return res;
}
void init(){
	fac[0]=1;invfac[0]=1;
	for(int i=1;i<=n;i++)fac[i]=1LL*fac[i-1]*i%P;
	invfac[n]=power(fac[n],P-2);
	for(int i=n;i>1;i--)invfac[i-1]=invfac[i]*i%P;
	return ;
}
LL ans=0;
LL C(LL n,LL m){
	return fac[n]*invfac[m]%P*invfac[n-m]%P;
}
int main(){
	n=read<int>();
	init();
	for(int i=n%2;i<=(n/2);i+=2){
		(ans+=2LL*C(n-i,i)%P*fac[n-i-1]%P)%=P;
	}
	//n=power(n,P-2);
	ans=(1LL*ans*n)%P;
	printf("%lld\n",ans);
	return 0;
}

后记

对于组合数来说,什么是关键:

  • 把握研究对象(比如插板法中的板子)。
  • 考虑做到不重不漏(前提是对排列和组合的深刻认识)。

\(P.S.\) 其中 \(E、F\) 题思路均来自@RenaMoe

但是总是感觉这种思路有点怪异(意义深究的话难以解释),所以有了:

另一种思路

枚举非空格格子的状态。

  • \(ans=2\times \sum_{i=2|i}^{n}i!\times (C(n-i,i) + C(n-i-1,i-1))\)

其中 \(i!\) 表示非空格子的全排列(并不等价于圆排列),\(C(n-i,i)\) 表示第一个格子不放空格的情况,\(C(n-i-1,i-1)\) 表示第一个格子放空格的情况。

个人感觉这种思路还是比较清楚的,也就是这个思路巧妙的运用了加法原理使得思路显而易见。

Updated on 2021/10/26

枚举的 \(i\) 并不是空格数,而是 \(A,B\) 的总和数,括号里加起来的方案数是把空格插进去的方案数(第 \(1\) 和第 \(n\) 个位置不能同时放)。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
template <typename T>
inline T read(){
	T x=0;char ch=getchar();bool fl=false;
	while(!isdigit(ch)){
		if(ch=='-')fl=true;ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
	return fl?-x:x;
}
#define int long long
#define LL long long
const int maxn = 1e6 + 10;
const LL P = 1e9+7;
LL fac[maxn],invfac[maxn];
int n;
LL power(LL a,LL b){
	LL res=1;
	while(b){
		if(b&1)res=1LL*res*a%P;
		a=1LL*a*a%P;
		b>>=1;
	}
	return res;
}
void init(){
	fac[0]=invfac[0]=1;
	for(int i=1;i<=n;i++)fac[i]=1LL*fac[i-1]*i%P;
	invfac[n]=power(fac[n],P-2);
	for(int i=n;i>1;i--)invfac[i-1]=1LL*invfac[i]*i%P;
}
LL ans=0;
LL C(int n,int m){
	if(n<m)return 0;
	return fac[n]*invfac[m]%P*invfac[n-m]%P;
}
LL f(int n,int m){
	return (C(n,m)+C(n-1,m-1))%P;
}
signed main(){
	n=read<int>();
	init();
	for(int i=2;i<=n;i+=2){
		//cerr<<ans<<endl;
		(ans+=1LL*fac[i]*f(i,n-i)%P)%=P;
	}
	(ans*=2LL)%=P;
	printf("%lld\n",ans);
	return 0;
}
posted @ 2021-08-12 17:31  ¶凉笙  阅读(36)  评论(0编辑  收藏  举报