牛客周赛 Round 29 (A,B,C,D,E,F)

这场难度控制的特别出色,不难但是都很有意思,尤其是E这个构造部分。比赛链接官方视频讲解

AB没有用到什么算法,C是个字符串处理,D是中位数,E是构造,F是概率DP。


A 小红大战小紫

思路:

比大小,没什么好说的

code:

#include <iostream>
#include <cstdio>
using namespace std;

int main(){
	int a,b;
	cin>>a>>b;
	puts((a==b)?"draw":(a>b)?"kou":"yukari");
	return 0;
} 

B 小红的白日梦

思路:

如果一天没有梦,那么这天就没有贡献,如果有一个,就一定放到白天,产生两点贡献,如果有两个,产生三点贡献。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int n,ans;
string a,b;

int main(){
	cin>>n>>a>>b;
	for(int i=0;i<n;i++){
		if(a[i]=='Y' || b[i]=='Y')ans+=2;
		if(a[i]=='Y' && b[i]=='Y')ans++;
	}
	cout<<ans<<endl;
	return 0;
}

C 小红的小小红

思路:

因为一定有“xiao”和“hong”这两个串,然后把它们取出来放在一起,其他的字符串不用管,所以可以用string。

string里有一个成员函数叫find(str),它可以从字符串中找到子串str,并返回它的首地址下标。然后还有一个成员函数erase(pos,len),它可以把字符串从pos下标位置开始后len个字符删掉。用这两个函数可以把“xiao”和“hong”这两个串剔出来,之后放到字符串末尾即可。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

string a;

int main(){
	cin>>a;
	a.erase(a.find("xiao"),4);
	a.erase(a.find("hong"),4);
	a+="xiaohong";
	cout<<a;
	return 0;
}

D 小红的中位数

思路:

中位数的定义是 一组数据中排在中间位置的数,如果数据量是偶数,则中位数是中间两个数的平均值

所以可以分情况来看,先对数组排序,如果一开始n为偶数,中位数就是第 n 2 \frac n 2 2n n 2 + 1 \frac n 2+1 2n+1 个数和的一半,去掉一个变成奇数。如果删掉的那个是前 n 2 \frac n 2 2n 的数,中位数相当于会右移一位,也就是原来的第 n 2 + 1 \frac n 2+1 2n+1 个,如果删掉的是后 n 2 \frac n 2 2n 的数,则为第 n 2 \frac n 2 2n 个。(其实说白了就是要找第 n 2 \frac {n} 2 2n 个元素,如果前面的数消失了,就顺位往后找一位即可。)

如果一开始n为奇数,中位数就是第 ⌈ n 2 ⌉ \left\lceil\frac n 2\right\rceil 2n 个数,去掉一个变成偶数。如果删掉的那个是前 ⌊ n 2 ⌋ \left\lfloor\frac n 2\right\rfloor 2n 的数,中位数是第 ⌊ n 2 ⌋ + 1 \left\lfloor\frac n 2\right\rfloor+1 2n+1 和第 ⌊ n 2 ⌋ + 2 \left\lfloor\frac n 2\right\rfloor+2 2n+2 个和的一半。如果删掉的是第 ⌈ n 2 ⌉ \left\lceil\frac n 2\right\rceil 2n,中位数是第 ⌊ n 2 ⌋ \left\lfloor\frac n 2\right\rfloor 2n 和第 ⌊ n 2 ⌋ + 2 \left\lfloor\frac n 2\right\rfloor+2 2n+2 个和的一半。如果删掉的是后 ⌈ n 2 ⌉ \left\lceil\frac n 2\right\rceil 2n的数,中位数是第 ⌊ n 2 ⌋ \left\lfloor\frac n 2\right\rfloor 2n 和第 ⌊ n 2 ⌋ + 1 \left\lfloor\frac n 2\right\rfloor+1 2n+1 个和的一半。(其实说白了就是要找第 ⌊ n 2 ⌋ \left\lfloor\frac {n} 2\right\rfloor 2n ⌊ n 2 ⌋ + 1 \left\lfloor\frac {n} 2\right\rfloor+1 2n+1 个元素,如果第 ⌊ n 2 ⌋ \left\lfloor\frac {n} 2\right\rfloor 2n 前面的数消失了,就两个数都顺位往后找一位即可,第 ⌊ n 2 ⌋ + 1 \left\lfloor\frac {n} 2\right\rfloor+1 2n+1 个丢失了就这一个数向后找一位)

只看理论容易迷糊,用实例来手推的话写起来很快。

题解的思路是 对数组排好序后从小到大枚举每个元素,把答案和这个元素的原来下标绑定一下,根据原来下标排好序后依次输出答案即可。但是找中位数的思路和上面是一致的

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;

int n,a[maxn],b[maxn];

int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	
	int tn;
	double ans;
	if(n&1){
		for(int i=1;i<=n;i++){
			tn=(n-1)/2;
			if(a[i]<=b[tn])ans=(b[tn+1]+b[tn+2])/2.0;
			else if(a[i]==b[tn+1])ans=(b[tn]+b[tn+2])/2.0;
			else ans=(b[tn]+b[tn+1])/2.0;
			printf("%.1lf\n",ans);
		}
	}
	else {
		for(int i=1;i<=n;i++){
			tn=n/2;
			if(a[i]<=b[tn])ans=b[tn+1];
			else ans=b[tn];
			printf("%.1lf\n",ans);
		}
	}
	
	return 0;
}

E 小红构造数组

思路:

有意思的来了。

首先需要先对x分解质因数,统计每个质因数和它的个数。然后用前面的质因数尝试构造出一个相邻两数不同的数列。分解质因数统计个数这一步可以用素数筛,也可以开方暴力,因为合数在分解之前,它的质因数一定会被分解到,所以开方暴力不需要判断素数,建议使用,很好写。

问题在于如何构造,因为 1 ≤ x ≤ 1 0 13 1\le x\le 10^{13} 1x1013 最多也就四五十个质因数,所以很暴力的做法都不会超时(别写dfs就行):

  1. (赛时的想法,有点丑陋)直接贪心,每次插入一种质因数,如果序列不合法,就从第一对不合法的位置开插。如果序列合法,那就隔一个数插一个就行了。如果到了末尾,就回到开始继续插。举个例子:
    如果有5个2,3个3,3个5,2个7可以这样插入:
    先插入2,得到2 2 2 2 2
    再插入3,从第一对不合法位置开始,得到2 3 2 3 2 3 2 2
    再插入5,从第一对不合法位置开始,查到最后再循环回来,得到5 2 3 2 3 2 3 2 5 2 5
    再插入7,因为这时候合法了,所以直接插入,得到7 5 7 2 3 2 3 2 3 2 5 2 5
    因为每次插入都是隔一个数,前面插过的数一定和当前的数不一致,所以可以保证插过的地方一定合法,不合法的原因在于插一圈回来和自己重复了,因此一次只存在一串不合法的位置,我们直接贪心地从第一对不合法的位置开插,可以保证首先消除的一定是不合法的位置,之后再随便插都可以。因此保证了正确性,如果插完了都还有不合法的位置,那么一定不存在解。(很难写)
  2. 题解的做法,可以给次小的质因数个数补上其他的更小的质因数,把它补到至少最大值-1,这样一轮中可以先输出一个最大的质因数,再输出一个次大的质因数,再输出其他剩余的质因数各一个,这样一轮之中输出的数各不相同,一定不重复。因为每一轮至少可以输出一个最大,一个最小,所以轮与轮中间是不会有重复的。如下图:
    请添加图片描述
    但是这样比较难写,考虑到其实就是随便拿一个质因数和最大个数抵消,直到最大个数和次大个数相差为1或者相等。所以我们可以每一轮拿出最大个数和另外一个质因数,两个数各取出一个相互抵消,这里我们可以拿次大个数,这样就是题解的做法了。判定无解条件也很显然了,就是最大个数的质因数比剩下的所有质因数个数+1还大,就说明凑不出来最大和次大差1了。
    请添加图片描述
  3. 假如一共有sum个质因数,有解的判定条件和上面一致,即最大个数的质因数小于等于剩下的所有质因数个数+1。把所有质因数按次数排好,比如3 3 3 2 2 5,然后先向前n个位置中的奇数位置按顺序放数,放满后再从偶数位置开始放,最后看一下有没有不合法位置即可。比如上面的就可以变成3 2 3 2 3 5。
    评论区有人提醒这也是一种贪心思想。假如最多的质因数个数为 x x x,次多的为 y y y,一共有 s u m sum sum 个。首先先把第一多的数放进奇数位置,因为有解的话, x x x 一定 ≤ \le 剩下的所有质因数个数+1,也就是 x ≤ s u m − x + 1 x\le sum-x+1 xsumx+1,所以 x ≤ ⌊ s u m + 1 2 ⌋ = ⌈ s u m 2 ⌉ = x\le \left\lfloor\frac {sum+1} 2\right\rfloor=\left\lceil\frac {sum} 2\right\rceil= x2sum+1=2sum= 奇数位置,所以奇数位置一定放得下。之后放入次多的数,奇数位置放完就从偶数位置开始继续放,如果要和奇数位置的这个数重复的话,次多的数一定需要至少奇数位置个数个这个质因数(偶数位置的数每个位置前移一位就和所有奇数位置重合了),即 y = ⌈ s u m 2 ⌉ y=\left\lceil\frac {sum} 2\right\rceil y=2sum,但这是不可能发生的。因为如果 x = y = ⌈ s u m 2 ⌉ = s u m 2 x=y=\left\lceil\frac {sum} 2\right\rceil=\frac {sum} 2 x=y=2sum=2sum y y y 就不可能从奇数位置开始放了,而其他情况都不会满足有 y = ⌈ s u m 2 ⌉ y=\left\lceil\frac {sum} 2\right\rceil y=2sum 个,最大和次大都不可能,后面就更没可能了,因此这样贪心是正确的。

这种题有很多种解法,本人愚钝不能参悟,只能整理出这三种解法。万无禁忌,千变万化,这也是算法竞赛的诱人之处吧。

code:

第一种思路,贪心,用的循环双链表,注意特判x=1。由于x可能有个大素数因子,所以要开longlong,否则会爆吃一串WA

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e7+5;
const int maxm=10005;

ll x;

ll prime[maxn],counter;
bool vist[maxn];
void Eular(int n){
	for(int i=2;i<=n;i++){
		if(!vist[i])prime[++counter]=i;
		for(int j=1,p=prime[1];j<=counter && i*p<=n;p=prime[++j]){
			vist[i*p]=true;
			if(i%p==0)break;
		}
	}
}

pair<ll,int> d[maxm];//质因数,个数 
int ct,n;
int nxt[maxm],pre[maxn],cnt;
ll a[maxm];

int main(){
	Eular(1e7);
	cin>>x;
	if(x==1){
		printf("-1");
		return 0;
	}
	for(int i=1,p=prime[1];1ll*p*p<=x;p=prime[++i]){
		if(x%p==0){
			d[++ct].first=p;
			while(x%p==0){
				d[ct].second++;
				x/=p;
			}
			n+=d[ct].second;
		}
	}
	if(x!=1){
		d[++ct].first=x;
		d[ct].second++;
		n+=d[ct].second;
	}
	sort(d+1,d+ct+1,[](pair<ll,int> x,pair<ll,int> y){return x.second>y.second;});
	
	int p=0;
	for(int i=1;i<=ct;i++){
		p=nxt[0];
		while(p && a[p]!=a[nxt[p]])p=nxt[p];
		for(int j=1;j<=d[i].second;j++){
			a[++cnt]=d[i].first;//向p后插入
			pre[cnt]=p;
			nxt[cnt]=nxt[p];
			pre[nxt[p]]=cnt;
			nxt[p]=cnt;
			p=nxt[cnt]; 
		}
	}
	bool f=false;
	for(int p=nxt[0];nxt[p];p=nxt[p]){
		if(a[p]==a[nxt[p]]){
			f=true;
			break;
		}
	}
	if(f){
		printf("-1");
		return 0;
	}
	printf("%lld\n",n);
	for(int p=nxt[0];p;p=nxt[p]){
		printf("%lld ",a[p]);
	}
	
	return 0;
}

第二种思路:
用堆来存储(题解用multiset,其实是一样的),每次取出最大和次大各输出一个,然后放回堆。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long ll;

ll x;

priority_queue<pair<int,ll> > h;//个数 质因数 
int sum;

int main(){
	cin>>x;
	if(x==1){
		printf("-1");
		return 0;
	}
	for(ll i=2;1ll*i*i<=x;i++){
		if(x%i==0){//这里不用判素数,因为组成合数的素数早就被除掉了
			pair<int,ll> t(0,0);
			t.second=i;
			while(x%i==0){
				t.first++;
				x/=i;
			}
			sum+=t.first;
			h.push(t);
		}
	}
	if(x!=1){
		sum++;
		h.push(make_pair(1,x));
	}
	
	if(h.top().first>sum-h.top().first+1){
		printf("-1");
		return 0;
	}
	printf("%d\n",sum);
	while(!h.empty()){
		pair<int,ll> t1=h.top(),t2;
		h.pop();
		if(!h.empty()){
			t2=h.top();
			h.pop();
			printf("%lld %lld ",t1.second,t2.second);
			t1.first--;
			t2.first--;
			if(t1.first)h.push(t1);
			if(t2.first)h.push(t2);
		}
		else {
			printf("%lld\n",t1.second);//就剩一个了 
			break;
		}
	}
	
	return 0;
}

第三种思路。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn=105;
typedef long long ll;

ll x;

pair<int,ll> d[maxn];//个数 质因数 
int cnt,sum;
ll a[maxn];

int main(){
	cin>>x;
	if(x==1){
		printf("-1");
		return 0;
	}
	for(ll i=2;1ll*i*i<=x;i++){
		if(x%i==0){
			d[++cnt].second=i;
			while(x%i==0){
				d[cnt].first++;
				x/=i;
			}
			sum+=d[cnt].first;
		}
	}
	if(x!=1){
		sum++;
		d[++cnt]=make_pair(1,x);
	}
	sort(d+1,d+cnt+1,greater<pair<int,ll> >());
	
	if(d[1].first>sum-d[1].first+1){
		printf("-1");
		return 0;
	}
	printf("%d\n",sum);
	int i=1;
	for(int p=1;p<=sum;p+=2){
		a[p]=d[i].second;
		--d[i].first;
		if(d[i].first==0)i++;
	}
	for(int p=2;p<=sum;p+=2){
		a[p]=d[i].second;
		--d[i].first;
		if(d[i].first==0)i++;
	}
	for(int j=1;j<=sum;j++)
		printf("%lld ",a[j]);
	
	return 0;
}

F 小红又战小紫

思路:

虽说概率DP给人的感觉就是吓人,但是这个还挺好写的。

首先看到有两个技能,一个是随机一个石子堆石子-1,另一个技能是全体石子堆石子-1。而且题目说了每堆石子初始个数小于等于2。那么可以推出两个很显然的结论:

  1. 如果所有石子堆石子个数都是1,这时不存在石子堆石子个数为2,一定使用技能2,这时胜率是1
  2. 如果存在至少一个石子堆石子个数为2,那么一定使用技能1(否则对方会面临所有石子堆石子个数都是1的情况,对方必胜)

假设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示某一方 面对 一共 i i i 堆非空石子堆, j j j 堆为2个石子的石子堆 的局面的胜率。那么

  1. j ≠ 0 j\not=0 j=0 时,使用技能1
    d p [ i ] [ j ] dp[i][j] dp[i][j] ( i − j ) / i (i-j)/i (ij)/i 概率转移到 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] ( i > j ) (i>j) (i>j)
    d p [ i ] [ j ] dp[i][j] dp[i][j] j / i j/i j/i 概率转移到 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] ( j > 0 ) (j>0) (j>0)
  2. j = 0 j=0 j=0 时,使用技能2,这时也就是边界条件 d p [ i ] [ 0 ] = 1 dp[i][0]=1 dp[i][0]=1

由于 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示某一方的胜率,而游戏是两个玩家交替进行,我们在一个局面,它的下一个局面的胜率实际上是对手在面对,dp值是对手的胜率,也就是我们在这个状态的败率。因此从下一个状态推过来时,需要用败率来乘转移概率再求和。也就是 d p [ 当前状态 ] = ∑ ( 1 − d p [ 下一个状态 ] ) ∗ 转移概率 dp[当前状态] = \sum (1-dp[下一个状态])*转移概率 dp[当前状态]=(1dp[下一个状态])转移概率

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e4+5;
const int mod=1e9+7;
typedef long long ll;

int n,cnt1,cnt2;
ll dp[maxn][maxn];

ll qpow(ll a,ll b){
	ll base=a,ans=1;
	while(b){
		if(b&1){
			ans*=base;
			ans%=mod;
		}
		base*=base;
		base%=mod;
		b>>=1;
	}
	return ans;
}
inline ll inv(ll x){
	return qpow(x,mod-2);
}

int main(){
	cin>>n;
	for(int i=1,t;i<=n;i++){
		cin>>t;
		if(t==1)cnt1++;
		if(t==2)cnt2++;
	}
	for(int i=0;i<=n;i++){
		for(int j=0;j<=min(i,cnt2);j++){
			if(i>0)dp[i][j]+=(i-j)*inv(i)%mod*((1-dp[i-1][j]+mod)%mod)%mod;//拿石子1的堆 
			if(j>0)dp[i][j]+=j*inv(i)%mod*((1-dp[i][j-1]+mod)%mod)%mod;//拿石子2的堆 
			else dp[i][j]=1;
			dp[i][j]%=mod;
		}
	}
	
	cout<<dp[n][cnt2];
	return 0;
}
  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值