【AtCoder】AtCoder Grand Contest 020 解题报告
\(A\):Move and Win(点此看题面)
- 一张\(n\)个格子的纸条,初始对弈双方棋子分别在\(A,B\)。
- 轮到一人操作时,他可以将棋子向左或向右移到一个空格,若不能操作就输了。
- 求谁能赢。
- \(1\le A<B\le n\le100\)
签到题
考虑不可能两个人都向两边走,而一人向内一人向外时二者距离不变。
因此只需考虑两者都向中间走,那么假设甲碰到了乙,则乙就只能不断往边上走,必输。
那么只要判下两者间距离奇偶性就好了。
代码:\(O(1)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
using namespace std;
int n,A,B;
int main()
{
return scanf("%d%d%d",&n,&A,&B),puts((A^B)&1?"Borys":"Alice"),0;//判下距离奇偶性
}
\(B\):Ice Rink Game(点此看题面)
- 初始有若干人,第\(i\)轮游戏给出一个\(a_i\),要求当前人数减小为最大的\(a_i\)倍数。
- \(n\)轮之后只剩\(2\)人,问可能的初始人数最小值和最大值。
- \(n\le10^5\)
倒推
这种题目显然倒着推一遍就解决了。。。
设当前可能人数范围为\([l,r]\),该次操作的数为\(a_i\)。
则这次操作前可能的人数范围就是\([\lceil\frac l{a_i}\rceil\times a_i,(\lfloor\frac r{a_i}\rfloor+1)\times a_i-1]\)。
代码:\(O(n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define LL long long
using namespace std;
int n,a[N+5];
int main()
{
RI i;LL l=2,r=2;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",a+i);//初始人数为2
for(i=n;i;--i) if((l=((l-1)/a[i]+1)*a[i])>(r=(r/a[i]+1)*a[i]-1)) return puts("-1"),0;//倒推
return printf("%lld %lld\n",l,r),0;//输出答案
}
\(C\):Median Sum(点此看题面)
- 给定一个长度为\(n\)的序列\(A\),求所有子序列和的中位数。
- \(n,A_i\le2000\)
中位数
考虑中位数有什么性质?
假设我们算入空集,则发现在这题中任意一种选法与其补集的和都是\(\sum_{i=1}^nA_i\)。
因此算入空集的中位数就是\(\frac{\sum_{i=1}^nA_i}2\)。
那么少了空集之后的中位数自然就是大于等于\(\frac{\sum_{i=1}^nA_i}2\)的第一个数了。
\(bitset\)优化\(01\)背包
这种问题显然是一个\(01\)背包问题,然而原本的问题并不好优化。
现在相当于只需要判一个数能否得到,直接\(bitset\)优化就好了。
代码:\(O(\frac{n^3}{32})\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000
using namespace std;
int n;bitset<N*N+5> s;
int main()
{
RI i,x,t=0;for(s.set(0),scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&x),t+=x,s|=s<<x;//bitset优化01背包
for(i=t+1>>1;i<=n*N;++i) if(s.test(i)) return printf("%d\n",i),0;//找到第一个大于等于t/2的数
}
\(D\):Min Max Repetition(点此看题面)
- 给定\(A,B\),求一个\(A\)个
A
和\(B\)个B
组成的字符串,满足:- 连续相同字符个数的最大值最小。
- 在此基础上,字典序最小。
- 输出该字符串\(C\sim D\)位。
- 数据组数\(\le10^3,A,B\le5\times10^8,D-C+1\le100\)
最优情况
当\(A<B\),只要交换\(A,B\),将得到的字符串翻转并反转\(A,B\)就可以转化成\(A>B\)的情况。
所以我们假设\(A\ge B\)。
先求出连续相同字符个数的最大值的最小值,显然将\(B\)尽可能均匀地插入\(A\)中肯定能得到最小值,所以\(t=\lceil\frac{A}{B+1}\rceil\)。
然后经过一定的推导发现最优情况应如下所示:
其中若干指的是个数在\([1,t]\)中。
代码实现
推一推\(x\)的取值范围:
因为我们要最大化\(x\),所以只需考虑式子的左半边,移项得到:
显然这个东西没法直接推导,于是我们考虑二分出\(x\)的值。
考虑最优情况可以划分为四部分,于是我们求出前三段长度\(a=x\times(t+1)\),\(b=A-x\times t-(\lceil\frac{B-x}t\rceil-1)\),\(c=(B-x-1)\mod t+1\)。
求第\(i\)位的值时根据它落在序列中的哪一块分类讨论:
- \(i\in[1,a]\):如果\(i\mod(t+1)\not=0\)则为
A
,否则为B
。 - \(i\in(a,a+b]\):
A
。 - \(i\in(a+b,a+b+c]\):
B
。 - \(i\in(a+b+c,A+B]\):如果\(i\mod(t+1)=1\)则为
A
,否则为B
。
代码:\(O(100T)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
using namespace std;
int A,B,C,D,t,x,a,b,c;char s[105];
I int Find(CI A,CI B,CI t)//二分出x的值
{
RI l=0,r=A,mid;W(l<r) mid=l+r+1>>1,1LL*t*mid+(B-mid-1)/t+1<=A?l=mid:r=mid-1;return l;
}
I void Work(CI A,CI B,CI C,CI D)//求解
{
t=(A-1)/(B+1)+1,x=Find(A,B,t),a=x*(t+1),b=A-x*t-(B-x-1)/t,c=(B-x-1)%t+1;//求出三个关键点
for(RI i=C;i<=D;++i) s[i-C+1]=i<=a?(i%(t+1)?'A':'B'):(i<=a+b?'A':(i<=a+b+c?'B':((i-a-b-c)%(t+1)==1?'A':'B')));//根据落在哪一块分类讨论
}
int main()
{
RI Tt,i,j;scanf("%d",&Tt);W(Tt--)
{
if(scanf("%d%d%d%d",&A,&B,&C,&D),A>=B) Work(A,B,C,D);//如果A≥B直接做
else for(Work(B,A,A+B-D+1,A+B-C+1),i=1;i<=D-C+2-i;++i)//如果A<B转化为A>B
s[i]=131-s[i],i^(D-C+2-i)&&(s[D-C+2-i]=131-s[D-C+2-i]),swap(s[i],s[D-C+2-i]);//翻转并反转A,B
s[D-C+2]=0,puts(s+1);//输出答案
}return 0;
}
\(E\):Encoding Subsets(点此看题面)
- 对于一个\(01\)串,你可以把\(n\)个连续相同串\(A\)压缩成一个
(Axn)
(算作一个字符)。 - 给定\(01\)串\(S\),求\(S\)所有子集压缩方案数之和(这里的子集指二进制数\(S\)的子集)。
- \(|S|\le100\)
暴搜
假设在字符串固定的情况下,我们设\(f_{l,r}\)表示区间\([l,r]\)的压缩方案数,\(g_{l,r}\)表示区间\([l,r]\)压缩为一个字符的方案数,得到\(DP\):
然后考虑字符串不固定,其实更加简单了。
因为任意循环节都是有可能的,所以\(DP\)求\(g\)的时候我们只要求出每一段的并值即可。
代码:\(O(n^5+n^2\times2^{\frac n8})\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define X 998244353
using namespace std;
string s;map<string,int> f,g;
I int F(Con string& s);
I int G(Con string& s)//DP求G
{
if(s=="") return 1;if(s=="0") return 1;if(s=="1") return 2;if(g.count(s)) return g[s];//判边界
RI i,j,k,x,l=s.length(),t=0;for(i=1;i^l;++i) if(!(l%i))//枚举循环节
{string ns="";for(j=0;j^i;++j) {for(x=49,k=j;k<l;k+=i) x&=s[k];ns+=x;}t=(t+F(ns))%X;}//求出每一段的并值继续DP
return g[s]=t;//记忆化
}
I int F(Con string& s)//DP求F
{
if(s=="") return 1;if(f.count(s)) return f[s];//判边界
RI i,l=s.length(),t=0;for(i=1;i<=l;++i) t=(1LL*G(s.substr(0,i))*F(s.substr(i,l))+t)%X;//将串断成两部分
return f[s]=t;//记忆化
}
int main()
{
return cin>>s,printf("%d\n",F(s)),0;
}
\(F\):Arcs on a Circle(点此看题面)
- 有\(n\)条长度为\(a_{1\sim n}\)的线段和一个周长为\(m\)的圆。
- 随机将这\(n\)条线段放到圆上(可以有重叠),问圆上所有点都被覆盖的概率。
- \(n\le6,m\le50\)
实数离散化
一个诡异的套路?
考虑线段的长度都是整数,而在比大小时整数部分是很好比较的,关键是小数部分。
然后我们发现对于小数部分我们只需要知道其相对大小,而不需要知道其具体值。
于是,我们对于小数部分离散化。
具体实现时其实是全排列暴枚小数部分的相对大小。
这样一来一共就只有\(n\times m\)个点了。
\(DP\)
接下来的\(DP\)应该是很简单的。
首先我们把最长的线段左端点作为原点,这样就可以化圆为链了。
然后设\(f_{i,j,k}\)为处理完左端点小于等于\(i\)的线段,最大右端点为\(j\),已用线段状压为\(k\)的方案数。
由于我们对小数部分离散化过了,所以每一个左端点只可能有某一条线段。
\(DP\)方程就是:
具体实现中\(i\)这一维完全可以直接删去。
代码:\(O((n-1)!\times (nm)^2\times 2^{n-1})\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 6
#define M 50
using namespace std;
int n,m,a[N+5];long long f[N*M+5][1<<N];
int main()
{
RI i;for(scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%d",a+i);
RI j,k,x,l=1<<n-1,res=0;long long ans=0;sort(a+1,a+n+1);do
{
for(j=1;j<=n*m;++j) for(k=0;k^l;++k) f[j][k]=0;//清空
for(f[n*a[n]][0]=i=1;i<=n*m;++i) if(x=i%n) for(j=i;j<=n*m;++j)//每个左端点只会对应一条线段
for(k=0;k^l;++k) !(k>>x-1&1)&&(f[min(n*m,max(j,i+n*a[x]))][k|(1<<x-1)]+=f[j][k]);//转移
++res,ans+=f[n*m][l-1];
}while(next_permutation(a+1,a+n));//暴枚实数部分相对大小
long double t=1.0*ans/res;for(i=1;i^n;++i) t/=m;return printf("%.15Lf\n",t),0;//概率=合法方案数/总方案数
}