这场难度控制的特别出色,不难但是都很有意思,尤其是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} 1≤x≤1013 最多也就四五十个质因数,所以很暴力的做法都不会超时(别写dfs就行):
- (赛时的想法,有点丑陋)直接贪心,每次插入一种质因数,如果序列不合法,就从第一对不合法的位置开插。如果序列合法,那就隔一个数插一个就行了。如果到了末尾,就回到开始继续插。举个例子:
如果有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
因为每次插入都是隔一个数,前面插过的数一定和当前的数不一致,所以可以保证插过的地方一定合法,不合法的原因在于插一圈回来和自己重复了,因此一次只存在一串不合法的位置,我们直接贪心地从第一对不合法的位置开插,可以保证首先消除的一定是不合法的位置,之后再随便插都可以。因此保证了正确性,如果插完了都还有不合法的位置,那么一定不存在解。(很难写) - 题解的做法,可以给次小的质因数个数补上其他的更小的质因数,把它补到至少最大值-1,这样一轮中可以先输出一个最大的质因数,再输出一个次大的质因数,再输出其他剩余的质因数各一个,这样一轮之中输出的数各不相同,一定不重复。因为每一轮至少可以输出一个最大,一个最小,所以轮与轮中间是不会有重复的。如下图:
但是这样比较难写,考虑到其实就是随便拿一个质因数和最大个数抵消,直到最大个数和次大个数相差为1或者相等。所以我们可以每一轮拿出最大个数和另外一个质因数,两个数各取出一个相互抵消,这里我们可以拿次大个数,这样就是题解的做法了。判定无解条件也很显然了,就是最大个数的质因数比剩下的所有质因数个数+1还大,就说明凑不出来最大和次大差1了。
- 假如一共有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 x≤sum−x+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= x≤⌊2sum+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,这时不存在石子堆石子个数为2,一定使用技能2,这时胜率是1
- 如果存在至少一个石子堆石子个数为2,那么一定使用技能1(否则对方会面临所有石子堆石子个数都是1的情况,对方必胜)
假设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示某一方 面对 一共 i i i 堆非空石子堆, j j j 堆为2个石子的石子堆 的局面的胜率。那么
-
j
≠
0
j\not=0
j=0 时,使用技能1
d p [ i ] [ j ] dp[i][j] dp[i][j] 有 ( i − j ) / i (i-j)/i (i−j)/i 概率转移到 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][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][j−1] ( j > 0 ) (j>0) (j>0) - 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[当前状态]=∑(1−dp[下一个状态])∗转移概率
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;
}