ARC104 题解

目前进度:A~E


时隔两年 ARC 复活,爷青回!(不对两年前我 OI 好像才入门)

所以这算文艺复兴吗

刚做完 D 巅峰时刻 rk6,然后掉到 rk47 了/kk,然后就只能 orz ntf 了。

最后竟然 performance 能有 2700,上了蓝。


Portal

A - Plus Minus

和差问题。梦回小学/cy

(python 多香啊)

a,b=map(int,input().split())
print((a+b)//2,(a-b)//2)

B - DNA Sequence

complementary 当且仅当 \(\texttt A\) 数量等于 \(\texttt T\) 数量,\(\texttt C\) 数量等于 \(\texttt G\) 数量。

然后暴力枚举子串,对于每个左端点累加一下即可,平方复杂度。

#include<bits/stdc++.h>
using namespace std;
int n;
string a;
int main(){
	cin>>n>>a;
	int ans=0;
	for(int i=0;i<n;i++){
		int A=0,T=0,C=0,G=0;
		for(int j=i;j<n;j++){
			if(a[j]=='A')A++;
			if(a[j]=='T')T++;
			if(a[j]=='C')C++;
			if(a[j]=='G')G++;
			ans+=A==T&&C==G;
		}
	}
	cout<<ans;
	return 0;
}

C - Fair Elevator

质量高起来了(

有一个电梯要经过 \(2n\) 楼,共有 \(n\) 个人,到每楼时都恰好有一个人上或下。需要满足若两个人存在时刻同时在电梯上,则它们在电梯上的时间相等。给定挖掉一些数据的每个人上下电梯的时间,求是否能补全成合法数据。

\(n\in[1,100]\)

我们考虑补全「对于每楼,上 / 下了谁」这个长度为 \(2n\) 的数组,而非每个人的上下电梯时间。

很容易得到结论:一个「封闭」的区间一定长度为偶数,并且左半边全上,右半边全下,左半边的人平移过去就是右半边。于是我们考虑将整个数组分成若干个「封闭」的区间,然后 bool DP 就好了。

状态数线性,转移量线性,判一个区间是否可能补成封闭线性,总复杂度三方。

#include<bits/stdc++.h>
using namespace std;
const int N=100;
int n;
int a[N+1],b[N+1];
int cnt[2*N+1];
int per[2*N+1];
bool dp[2*N+1];
bool chk(int l,int r){
	for(int i=l;i<=r;i++){
		if(per[i]<0&&~a[-per[i]]&&a[-per[i]]<l)return false;
		if(per[i]>0&&~b[per[i]]&&b[per[i]]>r)return false;
	}
	int half=(r-l+1)/2;
	for(int i=l;i<=l+half-1;i++){
		if(per[i]){
			if(per[i]<0)return false;
		}
		if(per[i+half]){
			if(per[i+half]>0)return false;
		}
		if(per[i]&&per[i+half]){
			if(per[i]+per[i+half])return false;
		}
	}
	return true;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i];
		if(~a[i]&&~b[i]&&a[i]>=b[i])return puts("No"),0;
		if(~a[i])cnt[a[i]]++;
		if(~b[i])cnt[b[i]]++;
		if(~a[i])per[a[i]]=i;
		if(~b[i])per[b[i]]=-i;
	}
	for(int i=1;i<=2*n;i++)if(cnt[i]>1)return puts("No"),0;
	dp[0]=true;
	for(int i=1;i<=2*n;i++){
		for(int j=i&1?2:1;j<=i;j+=2)dp[i]|=dp[j-1]&&chk(j,i);
	}
	puts(dp[2*n]?"Yes":"No");
	return 0;
}

D - Multiset Mean

给定 \(n,k\),求 \(\forall x\in[1,n]\)\(1\sim n\) 每个数分别可以选 \(0\sim k\) 次组成的可重集中,平均数等于 \(x\) 的数量。答案对给定的 \(p\) 取模。

\(n,k\in[1,100]\)

注意到一个可重集 \(s\) 平均数为 \(x\)\(\sum\limits_{i\in s}i=|s|x\),当且仅当 \(\sum\limits_{i\in s}(i-x)=0\)

对于一个 \(x\)\(i-x\) 的分布规律显然是 \(\cdots,-3,-2,-1,0,1,2,3,\cdots\)。考虑 \(0\) 的两边,上面那个等式显然当且仅当两边各选的和的绝对值相等,然后 \(0\) 可以随便选。

又因为两边的绝对值是对称的,所以我们可以只算出这个东西:在 \(1\sim i\) 中,每个数可以选 \(0\sim k\) 个,和为 \(j\) 的方案数。这显然是个多重背包,\(i\)\(\mathrm O(n)\) 的,\(j\)\(\mathrm O\!\left(n^2k\right)\) 的。如果我们能做到常数转移,总复杂度 \(\mathrm O\!\left(n^3k\right)\),时限又是 \(4\mathrm s\),再配上 AtC 的机子,肯定能 AC 了。

对于一般意义上的求最大价值的多重背包,我们可以用二进制分解或者单调队列优化,而在本题求方案数的多重背包中完全没有这么麻烦。注意到加法是可逆的,我们只需要用累加代替单调队列,将单调队列的弹出改成减法即可。

#include<bits/stdc++.h>
using namespace std;
const int N=100,K=N;
int n,k,mod;
void add(int &x,int y){x+=y,x>=mod&&(x-=mod);}
int dp[N+1][N*N*K+1];
int sum[N];
int main(){
	cin>>n>>k>>mod;
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		memset(sum,0,sizeof(sum));
		for(int j=0;j<=n*n*k;j++){
			add(sum[j%i],dp[i-1][j]);
			if(j-(k+1)*i>=0)add(sum[j%i],mod-dp[i-1][j-(k+1)*i]);
			dp[i][j]=sum[j%i];
		}
	}
	for(int i=1;i<=n;i++){
		int ans=0;
		for(int j=0;j<=n*n*k;j++)add(ans,1ll*dp[i-1][j]*dp[n-i][j]%mod);
		printf("%lld\n",(1ll*ans*(k+1)-1)%mod);
	}
	return 0;
}

E - Random LIS

给定 \(n\) 个正整数 \(a_i\)。构造一个长度为 \(n\) 的序列 \(x\),使 \(x_i\)\([1,a_i]\cap\mathbb Z\) 内均匀随机。问 \(x\) 的 LIS 的期望。

\(n\in[1,6]\)

看完题面满脸不会做,然后看数据范围咋这么小。。

考虑枚举 \(x_{1\sim n}\) 的相对大小,因为是 \(n\leq6\) 所以不会太多。不难想到,枚举它们的相对大小关系,就是枚举排完序的 \(x'\)\(1\sim n\) 分别是原 \(x\) 中的第几个。然而我现场没有想到有 \(\leq\) 这种东西,直接枚举全排列了,导致最后没写出来。事实上,它们中可能有相等的,那么比如 \(1,2,(3,4,5),6\)\(1,2,(4,5,3),6\) 这两种就是一样的,会重复计算。于是我们需要改进一下枚举方法。

不难想到,这样就相当于,每一种状态先极大相等段分解,然后再考虑相等段之间的相对大小关系。也就是先将 \(n\) 个数不重不漏分成若干组,然后对组进行全排列。这个方案数好像叫啥 Fubini Number,\(n=6\) 时只有 \(4683\)\(720\to 4683\),悲)。不知道有没有啥好的枚举方法,我写的是枚举每个数的排名(可相同),然后对于每一种方案判合法(排名 \(1\sim mx\) 是否全出现过,其中 \(mx\) 是当前最大排名)。

然后考虑对于每一种相对大小关系,算出它对答案的贡献。显然同一种相对大小关系的 LIS 是固定的,这个随便 DP 就可以求出。那么我们只需要算出当前相对大小关系下 \(x\) 具体取值的方案数即可,最后与 LIS 乘起来再加到答案里最后除以总方案数即可得到期望值。

考虑如何求方案数。不难想到将 \(a\) 离散化,然后作为断点把整个值域分成几段,这样考虑起来方便许多。考虑对于某一种(就目前而言合法的)每个数所在段的情况,显然所在段不同即可直接肯定的比出大小,如果相同的话则有几种情况。对于每个段,我们数一下里面坐落着几个相等段,它们肯定不能互相相等,大小关系又是确定的,于是我们只需要在当前段中随意取出这么多个不同的数,排个序即可得到恰好一种方案。于是乘几个组合数(组合数 \(\dbinom xy\) 虽然 \(x\) 很大,但是 \(y\)\(\mathrm O(n)\) 的,所以可以 \(\mathrm O(n)\) 飞快算)即可得到当前每个数所在段的情况的方案数。然后枚举每个数所在段的情况,加一下即可。

全是爆搜,复杂度未知,不过如果能保证第二次爆搜是「极好」的(即每次 x==mx_id+1 的时候都一定是合法方案,这是很容易做到的),那么跑得飞快。

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
const int mod=1000000007;
int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1)res=1ll*res*x%mod;
		x=1ll*x*x%mod;
		y>>=1;
	}
	return res;
}
int inv(int x){return qpow(x,mod-2);}
const int N=6;
int n;
int fact[]={1,1,2,6,24,120,720},factinv[]={inv(1),inv(1),inv(2),inv(6),inv(24),inv(120),inv(720)};
int a[N+1];
vector<int> nums;
int ans;
int id[N+1],mx_id;
int sum;
int in_for_id[N+1],in[N+1];
int len(int x){return x==0?nums[x]:nums[x]-nums[x-1];}
int comb(int x,int y){
	if(x<y)return 0;
	int res=1;
	for(int i=x-y+1;i<=x;i++)res=1ll*res*i%mod;
	return 1ll*res*factinv[y]%mod;
}
void dfs0(int x=1){
	if(x==mx_id+1){
		for(int i=1;i<=n;i++)in[i]=in_for_id[id[i]];
		int prod=1;
		for(int i=1;i<=n;i++)if(nums[in[i]]>a[i])return;
		int cnt[N]={};
		for(int i=1;i<=mx_id;i++)cnt[in_for_id[i]]++;
		for(int i=0;i<nums.size();i++)prod=1ll*prod*comb(len(i),cnt[i])%mod;
		(sum+=prod)%=mod;
//		for(int i=1;i<=n;i++)cout<<in[i]<<" ";printf("|| %d\n",prod);
		return;
	}
	for(int i=in_for_id[x-1];i<nums.size();i++){
		in_for_id[x]=i;
		dfs0(x+1);
	}
}
void dfs(int x=1){
	if(x==n+1){
		vector<int> v;
		for(int i=1;i<=n;i++)v.pb(id[i]);
		sort(v.begin(),v.end());
		v.resize(unique(v.begin(),v.end())-v.begin());
		if(v.back()!=v.size())return;
		mx_id=v.size();
		int dp[N+1],mx=0;
		for(int i=1;i<=n;i++){
			dp[i]=1;
			for(int j=1;j<i;j++)if(id[j]<id[i])dp[i]=max(dp[i],dp[j]+1);
			mx=max(mx,dp[i]);
		}
		sum=0;
		dfs0();
//		for(int i=1;i<=n;i++)cout<<id[i]<<" ";puts("");cout<<mx<<" "<<sum<<"!\n";
		(ans+=1ll*mx*sum%mod)%=mod;
		return;
	}
	for(int i=1;i<=n;i++){
		id[x]=i;
		dfs(x+1);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],nums.pb(a[i]);
	sort(nums.begin(),nums.end());
	nums.resize(unique(nums.begin(),nums.end())-nums.begin());
	dfs();
	int tot=1;
	for(int i=1;i<=n;i++)tot=1ll*tot*a[i]%mod;
	cout<<1ll*ans*inv(tot)%mod;
	return 0;
}

然后看了 official solution 发现第二部分的那个爆搜可以有多项式复杂度解法?其实就是一个并不难的 DP。这个 DP 的精髓在哪里呢,它是如何把非多项式变成多项式的呢?注意到在某个相对大小关系下,考虑第 \(i\) 大的相等段的时候,只需要保证比第 \(i-1\) 大的相等段大即可,这样一来就有了阶段性。

所以也难怪,这的确是个比较弱智的 DP。很多 DP 都是这样,感受最明显的是状压 DP,先想当前状态与哪些量无关,哪些量有关,想办法搞出阶段性,然后就好 DP 了。

具体说就是 \(dp_{i,j}\) 表示考虑到第 \(i\) 大的相等段,第 \(i\) 大的相等段坐落在第 \(j\) 段的方案数。由于小于分成在同段和异段两种,所以需要枚举与 \(i\) 同段的极大相等段区间左端点,乘个组合数转移,这样是三方或者四方的,不管了。

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
const int inf=INT_MAX,mod=1000000007;
int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1)res=1ll*res*x%mod;
		x=1ll*x*x%mod;
		y>>=1;
	}
	return res;
}
int inv(int x){return qpow(x,mod-2);}
const int N=6;
int n;
int fact[]={1,1,2,6,24,120,720},factinv[]={inv(1),inv(1),inv(2),inv(6),inv(24),inv(120),inv(720)};
int a[N+1];
vector<int> nums;
int ans;
int id[N+1],mx_id;
int len(int x){return x==0?nums[x]:nums[x]-nums[x-1];}
int comb(int x,int y){
	if(x<y)return 0;
	int res=1;
	for(int i=x-y+1;i<=x;i++)res=1ll*res*i%mod;
	return 1ll*res*factinv[y]%mod;
}
int sol(){
	int dp[N+1][N+1]={},lim[]={inf,inf,inf,inf,inf,inf,inf};
	for(int i=1;i<=n;i++)lim[id[i]]=min(lim[id[i]],a[i]);
	for(int i=0;i<=nums.size();i++)dp[0][i]=1;
	for(int i=1;i<=mx_id;i++){
		for(int j=0;j<nums.size();j++)if(nums[j]<=lim[i])
			for(int k=i;k;k--){
				if(nums[j]>lim[k])break;
				(dp[i][j+1]+=1ll*comb(len(j),i-k+1)*dp[k-1][j]%mod)%=mod;
			}
//		cout<<dp[i][0]<<" "<<dp[i][1]<<" "<<dp[i][2]<<"!\n";
		for(int j=1;j<=nums.size();j++)(dp[i][j]+=dp[i][j-1])%=mod;
	}
//	for(int i=1;i<=n;i++)cout<<id[i]<<" ";printf("|| %d\n",dp[mx_id][nums.size()]);
	return dp[mx_id][nums.size()];
}
void dfs(int x=1){
	if(x==n+1){
		vector<int> v;
		for(int i=1;i<=n;i++)v.pb(id[i]);
		sort(v.begin(),v.end());
		v.resize(unique(v.begin(),v.end())-v.begin());
		if(v.back()!=v.size())return;
		mx_id=v.size();
		int dp[N+1],mx=0;
		for(int i=1;i<=n;i++){
			dp[i]=1;
			for(int j=1;j<i;j++)if(id[j]<id[i])dp[i]=max(dp[i],dp[j]+1);
			mx=max(mx,dp[i]);
		}
		(ans+=1ll*mx*sol()%mod)%=mod;
		return;
	}
	for(int i=1;i<=n;i++){
		id[x]=i;
		dfs(x+1);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],nums.pb(a[i]);
	sort(nums.begin(),nums.end());
	nums.resize(unique(nums.begin(),nums.end())-nums.begin());
	dfs();
	int tot=1;
	for(int i=1;i<=n;i++)tot=1ll*tot*a[i]%mod;
	cout<<1ll*ans*inv(tot)%mod;
	return 0;
}
posted @ 2020-10-04 20:45  ycx060617  阅读(379)  评论(0编辑  收藏  举报