AtCoder Beginner Contest 285 解题报告

时间:2023-01-16 11:08:14

\(\text{DaiRuiChen007}\)

Contest Link

A. Edge Checker 2

假设 \(a\ge b\),当且仅当 \(\left\lfloor\dfrac a2\right\rfloor=b\) 时成立

时间复杂度 \(\Theta(1)\)

#include<bits/stdc++.h>
using namespace std;
signed main() {
	int a,b;
	scanf("%d%d",&a,&b);
	if(a<b) swap(a,b);
	puts(a/2==b?"Yes":"No");
	return 0;
}

B. Longest Uncommon Prefix

对于每个 \(i\) 从小到大不断增加 \(l\) 的值并判断即可

时间复杂度 \(\Theta(n^2)\)

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5001;
char s[MAXN];
signed main() {
	int n;
	scanf("%d%s",&n,s+1);
	for(int i=1;i<n;++i) {
		int l=0;
		while(i+l+1<=n&&s[l+1]!=s[i+l+1]) ++l;
		printf("%d\n",l);
	}
	return 0;
}

C. abc285_brutmhyhiizp

\(n=|S|\),字符串下标从 \(1\) 开始

从最高位开始考虑,假设最高位的字母是 \(\texttt C\),那么最高位填 \(\texttt A,\texttt B\) 或空的字符串一定更小,这样的字符串总数是 \(3\times 26^{n-1}\) 个,而最高位填 \(\texttt C\) 的串继续比较下一位即可

对于第二位、第三位……也类似统计即可

时间复杂度 \(\Theta(n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=20;
char s[MAXN];
signed main() {
	int ans=0;
	scanf("%s",s+1);
	int n=strlen(s+1);
	for(int i=n,x=1;i>=1;--i,x*=26) ans+=(s[i]-'A'+1)*x;
	printf("%lld\n",ans);
	return 0;
}

D. Change Usernames

连接所有的 \(S_i\to T_i\),发现得到的图上每个点的出入度 \(\le 1\),因此这张图上只可能有若干个环和若干条链

注意到环一定不行,而链一定可行,因此对原图做拓扑排序判环即可

时间复杂度 \(\Theta(n\log n)\),瓶颈在用 map 实现字符串哈希上

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+1;
map <string,int> rec;
vector <int> G[MAXN];
int siz=0,deg[MAXN];
inline int id(string S) {
	if(rec.find(S)==rec.end()) rec[S]=++siz;
	return rec[S];
}
signed main() {
	int n;
	cin>>n;
	for(int i=1;i<=n;++i) {
		string u,v;
		cin>>u>>v;
		G[id(u)].push_back(id(v));
		++deg[id(v)];
	}
	queue <int> q;
	for(int i=1;i<=siz;++i) if(!deg[i]) q.push(i);
	while(!q.empty()) {
		int p=q.front(); q.pop();
		for(int v:G[p]) {
			--deg[v];
			if(!deg[v]) q.push(v);
		}
	}
	for(int i=1;i<=siz;++i) {
		if(deg[i]>0) {
			puts("No");
			return 0;
		}
	}
	puts("Yes");
	return 0;
}

E. Work or Rest

考虑 dp,用 \(dp_i\) 表示前 \(i\) 天在第 \(i\) 天休息时的最大价值,状态转移方程如下:

\[dp_i=\max_{j=1}^i\{dp_j+\operatorname{cost}(j,i)\} \]

其中 \(\operatorname{cost}(l,r)\) 表示在 \(l,r\) 两天休息,中间不休息的情况下 \(l+1\sim r-1\) 获得的最大价值,通过前缀和优化可以在 \(\Theta(1)\) 的时间内计算

注意到第 \(n\) 天可能和下周的第一天结合产生贡献,为了解决这个问题,我们不妨把有休假的日子设为第 \(1\) 天,这样答案就是 \(dp_{n+1}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5005;
int a[MAXN],sum[MAXN],dp[MAXN];
inline int cost(int l,int r) {
	int k=r-l-1;
	return sum[(k+1)/2]+sum[k/2];
}
signed main() {
	int n;
	scanf("%lld",&n);
	for(int i=1;i<=n;++i) {
		scanf("%lld",&a[i]);
		sum[i]=sum[i-1]+a[i];
	}
	memset(dp,-0x3f,sizeof(dp));
	dp[1]=0;
	for(int i=2;i<=n+1;++i) {
		for(int j=1;j<i;++j) {
			dp[i]=max(dp[i],dp[j]+cost(j,i));
		}
	}
	printf("%lld\n",dp[n+1]);
	return 0;
}

F. Substring of Sorted String

\(26\) 棵树状数组分别统计每个字母在特定区间中出现的次数

每次回答询问时先得到区间中的最小字母 \(lo\) 和最大字母 \(hi\),先判断字母 \(lo+1\sim hi-1\) 中的每个字母是不是都全部在 \([l,r]\) 中,然后简单模拟得到在每个字母对应的区间再判断这个区间是否全是该字母即可

时间复杂度 \(\Theta(|\Sigma|\times n\log n)\)\(\Sigma\) 为字符集

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+1;
int n,q;
struct BitTree {
	int tree[MAXN];
	inline int lowbit(int x) { return x&-x; }
	inline void Modify(int x,int v) {
		for(;x<=n;x+=lowbit(x)) tree[x]+=v;
	}
	inline int q(int x) {
		int ret=0;
		for(;x;x-=lowbit(x)) ret+=tree[x];
		return ret;
	}
	inline int Query(int l,int r) {
		return q(r)-q(l-1);
	}
}	A[26];
char str[MAXN];
signed main() {
	scanf("%d%s%d",&n,str+1,&q);
	for(int i=1;i<=n;++i) {
		A[str[i]-'a'].Modify(i,1);
	}
	while(q--) {
		int op;
		scanf("%d",&op);
		if(op==1) {
			int x; char c;
			scanf("%d %c",&x,&c);
			A[str[x]-'a'].Modify(x,-1);
			str[x]=c;
			A[str[x]-'a'].Modify(x,1);
		} else {
			bool ok=true;
			int l,r;
			scanf("%d%d",&l,&r);
			int lo=26,hi=0;
			for(int i=0;i<26;++i) {
				if(A[i].Query(l,r)>0) {
					lo=min(lo,i),hi=max(hi,i);
				}
			}
			int x=l;
			for(int i=lo;i<=hi;++i) {
				int k=A[i].Query(l,r);
				if(lo<i&&i<hi) if(k<A[i].Query(1,n)) ok=false;
				if(A[i].Query(x,x+k-1)!=k) ok=false;
				x+=k;
			}
			puts(ok?"Yes":"No");
		}
	}
}

G. Tatami

\(1\times 2\) 骨牌覆盖网格图立刻想到黑白染色建立二分图,对于已经被 \(1\times 1\) 覆盖的方格先删除,我们只需要为所有 \(c_{i,j}=2\) 的位置找到匹配即可,剩下的位置用 \(1\times 1\) 填补

正解好像是网络流,这里只提供一种乱搞做法:

对于每个 \(c_{i,j}=2\) 的没有匹配的点,直接在二分图上暴力搜出增广路,如果搜不出来增广路则输出 No

时间复杂度 \(\Theta(h^2w^2)\)

注意到实际上很难卡满时间复杂度,因此注意一下实现的常数(例如用时间戳标记对此复用 vis[] 数组避免过多的 memset 操作)即可通过本题

#include<bits/stdc++.h> 
using namespace std;
const int MAXN=301,MAXV=1e5+1;
vector <int> G[MAXV];
int tar[MAXV],vis[MAXV];
inline bool dfs(int x,int t) {
	if(vis[x]==t) return false;
	vis[x]=t;
	for(int p:G[x]) {
		if(vis[p]==t) continue;
		vis[p]=t;
		if(tar[p]==-1||dfs(tar[p],t)) {
			tar[p]=x,tar[x]=p;
			return true;
		}
	}
	return false;
}
const int dx[]={0,0,1,-1},dy[]={1,-1,0,0};
char a[MAXN][MAXN];
int id[MAXN][MAXN]; 
signed main() {
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%s",a[i]+1);
	for(int i=1,cnt=0;i<=n;++i) {
		for(int j=1;j<=m;++j) {
			id[i][j]=++cnt;
		}
	}
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=m;++j) {
			if(a[i][j]=='1'||(i+j)%2==0) continue;
			for(int k:{0,1,2,3}) {
				int x=i+dx[k],y=j+dy[k];
				if(x<1||x>n||y<1||y>m) continue;
				if(a[x][y]=='1') continue;
				G[id[i][j]].push_back(id[x][y]);
				G[id[x][y]].push_back(id[i][j]);
			}
		}
	}
	memset(tar,-1,sizeof(tar));
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=m;++j){
			if(a[i][j]=='2') {
				if(tar[id[i][j]]!=-1) continue;
				if(!dfs(id[i][j],id[i][j])) {
					puts("No");
					return 0;
				}
			}
		}
	}
	puts("Yes");
	return 0;
}

Ex. Avoid Square Number

显然立刻想到容斥,记 \(S_i\) 为至少存在 \(i\) 个平方数的方案数,那么得到:

\[\text{Answer}=\sum_{i=0}^n (-1)^i\times \binom ni\times S_i \]

那么原问题转化为求 \(S_i\)\(S_i\) 可以转化为对于每个质因数 \(p_j\),求出把 \(E_j\) 拆成至少 \(i\) 个偶数的方案数的总乘积

考虑一次性处理出对于所有 \(E_j\) 的答案,对使用质因子数量建立生成函数,即 \(F_i(x)=\sum_{k=0}^\infty x^k\times f_{i,k}\),其中 \(f_{i,k}\) 为把 \(k\) 拆成至少 \(i\) 个偶数的方案数,那么我们知道 \(S_i=\prod_{j=1}^n f_{i,E_j}\)

\(F_i(x)\) 的值也是一个非常经典的问题,推导形过程如下:

\[\begin{aligned} F_i(x) &=(x^0+x^2+x^4+x^6+\cdots)^i\times(x^0+x^1+x^2+x^3+\cdots)^{n-i}\\[2ex] &=\left(\dfrac{1}{1-x^2}\right)^i\times\left(\dfrac 1{1-x}\right)^{n-i}\\ &=\dfrac{1}{(1-x)^n\times (1+x)^i} \end{aligned} \]

注意到我们可以每次暴力卷积计算出 \(F_0(x)\),而每次转移 \(F_{i}(x)\to F_{i+1}(x)\) 只需要乘上 \((1+x)^{-1}=1-x+x^2-x^3+x^4\cdots\) 就可以得到下一个 \(F\)

\(w=\max_{i}^k\{E_i\}\),那么所有的多项式都只需要在 \(\bmod x^{w+1}\) 意义下进行

注意到乘 \(\dfrac 1{1-x}\) 等价于做前缀和,乘 \(\dfrac 1{1+x}\) 等价于做前缀差(不等于差分,可以自己推导一下),因此多项式操作的复杂度都是 \(\Theta(w)\)

求出 \(F_0(x)\) 的复杂度是 \(\Theta(nw)\),而接下来依次计算 \(F_1(x)\sim F_n(x)\) 的总复杂度也是 \(\Theta(nw)\),每次通过 \(F_i(x)\)\(S_i\) 的复杂度是 \(\Theta(k)\),执行 \(n\) 次,而容斥的复杂度是 \(\Theta(n)\)

综上,时间复杂度为 \(\Theta(nw+nk)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e4+1,MOD=1e9+7;
int n,k,E[MAXN],p[MAXN];
inline void sum(vector <int> &F) {
	for(int i=1;i<MAXN;++i) F[i]=(F[i]+F[i-1])%MOD;
}
inline void del(vector <int> &F) {
	for(int i=1;i<MAXN;++i) F[i]=(F[i]+MOD-F[i-1])%MOD;
}
int fac[MAXN],inv[MAXN];
inline int binom(int n,int m) {
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
inline int ksm(int a,int b,int m=MOD) {
	int ret=1;
	while(b) {
		if(b&1) ret=ret*a%m;
		a=a*a%m;
		b=b>>1;
	}
	return ret;
}
signed main() {
	scanf("%lld%lld",&n,&k);
	fac[0]=inv[0]=1;
	for(int i=1;i<=n;++i) fac[i]=fac[i-1]*i%MOD,inv[i]=ksm(fac[i],MOD-2);
	for(int i=1;i<=k;++i) scanf("%lld",&E[i]);
	vector <int> F(MAXN);
	F[0]=1;
	for(int i=1;i<=n;++i) sum(F);
	for(int i=0;i<=n;++i) {
		p[i]=1;
		for(int j=1;j<=k;++j) p[i]=(p[i]*F[E[j]])%MOD;
		del(F);
	}
	int ans=0;
	for(int i=0,f=1;i<=n;++i,f*=-1) {
		ans=(ans+MOD+f*binom(n,i)*p[i]%MOD)%MOD;
	}
	printf("%lld\n",ans);
	return 0;
}