题面
CSP-S 2019膜你赛
liqing
A
(a.cpp/in/out,1s,128MiB)
因为是一个立青,所以他对所有的知识一窍不通。
一天,他遇到了一道题,但他肯定不会啊,所以作为神犇的你能帮帮他吗?
给你一个个数的序列。会选择一个数,然后对这个序列进行若干次操作。一次操作是在这个序列中选择恰好个互不相同的数,并将它们从序列中删掉(之后就不能再选了)。
由于很无聊,所以他想知道对于每个,他最多能进行多少次操作。
输入格式
第一行仅一个正整数
第二行给出一个长度为的序列,保证其中每个整数
输出格式
有行,每一行仅一个整数。第行的整数代表着当时的答案
样例输入
3
2 1 2
样例输出
3
1
0
数据范围与约定
对于的数据:
对于的数据:
对于的数据:
对于的数据:
B
(b.cpp/in/out,1s,128MiB)
并没有听懂你上一题的解法,所以就自闭了。于是他想找些谜语来快乐一下,你听到他的要求后就给了他道谜语,这些谜语的答案要不是,要不是。
但是由于是个立青,所以他一题都不会做,只能瞎猜。。。你看他太过可怜,就告诉了他其中有道题目答案是,道题目答案是,并且每当回答完一道问题之后你就会马上告诉他这道题的正确答案。
现在想通过你的帮助来最大化他答对题目的数目,你能告诉他如果按最优策略的话他答对题目数目的期望值吗?
输入格式
仅一行两个正整数和。
输出格式
仅一行一个整数,表示期望值对取模后的结果
样例输入
1 1
样例输出
499122178
提示
样例解释:
第一个问题有的概率答对。而当他打完后,由于他已经知道了第一个问题的正确答案了,并且只有两个问题,所以第二个问题他有的概率答对。
所以输出的真实值是。
数据范围与约定
对于的数据:
对于的数据:
对于的数据:
另有的数据:
对于的数据:
C
(c.cpp/in/out,2s,256MiB)
因为是一个立青,所以他对所有的知识一窍不通。
一天,他遇到了一道题,但他肯定不会啊,所以作为神犇的你能帮帮他吗?
给你一棵以号节点为根的树,每个点有一个点权。
有次询问,每次询问给出两个数和,要你求到路径上最大的,保证是的祖先。(是异或的意思,是到简单路径的边数)
输入格式
第一行包含两个正整数和,分别代表了这棵树的点数和总询问次数
第二行有个整数,第个整数代表着。保证其中每个整数
接下来行描述了一棵树。每一行包含两个整数和,代表着树上的一条边
最后行,每一行包含两个整数和,代表着一次询问。保证是的祖先
输出格式
有行,第行代表着第次询问的答案
样例输入
5 3
0 3 2 1 4
1 2
2 3
3 4
3 5
1 4
1 5
2 4
样例输出
3
4
3
数据范围与约定
对于的数据:
:
:保证是的整数次幂
:保证图是一条链
:无
:保证所有的都相同
解题报告
(爆零记)
A
这道题我在考场上一眼就看出来是一道贪心题,可是考场上始终想不到正解。
最终迫不得已,写了一个的做法,先贴一下吧——真的很好写。
#include<cstdio>
#include<vector>
#include<cctype>
#include<cstring>
#include<algorithm>
#define R register
#define g getchar()
using namespace std;
const int N=1e6+10;
void qr(int &x) {
char c=g;x=0;
while(!isdigit(c))c=g;
while(isdigit(c))x=x*10+c-'0',c=g;
}
void write(int x) {
if(x/10) write(x/10);
putchar(x%10+'0');
}
vector<int>a;
int n,cnt[N],c[N],top,ans[N];
bool cmp(int x,int y) {return x>y;}
int main() {
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
qr(n);
for(R int i=1,x;i<=n;i++)
qr(x),cnt[x]++;
for(R int i=1;i<=n;i++)
if(cnt[i])
c[++top]=cnt[i];
sort(c+1,c+top+1,cmp); ans[1]=n; ans[top]=c[top];
for(R int i=2;i<top;i++) {
a.clear();
for(R int j=1;j<=i;j++)a.push_back(c[j]);
for(R int j=i+1;j<=top;j++) {
ans[i]+=a[i-1];
for(int k=0;k<i-1;k++)a[k]-=a[i-1];
a.pop_back();
a.insert(lower_bound(a.begin(),a.end(),c[j],cmp),c[j]);
}
ans[i]+=a[i-1];
}
for(R int i=1;i<=n;i++)
write(ans[i]),puts("");
return 0;
}
正解——果然贪心。
显然,只有每个数的出现次数有效(并不在意数是多少)
设为第k个答案(输出值),则容易发现
(单次取更多数的话,能进行的次数不会更多)
所以我们可以倒着求答案,因为倒着来答案非降,所以如果可以在判断一个数可不可以成为答案的话,就可以求出所有答案了。
本题的重点在于如何判断对于一个确定的,是否能执行次操作。
我们现在需要统计一下这条水平线下有多少个有用的单位格子(1*1)(表示每一个数)
怎么算呢?——如果一个数出现次数为,那么行的格子数就会增加1.最后做一个前缀和,就可以求出每条水平线下的格子数了。
重要引理
如果上述水平线下有个格子的话,那么能进行至少次操作,当且仅当
这个引理的必要性显然,我们只需要对其充分性进行证明。
证明:
物理是个好学科。
我们把水平线下的数按序排成一排,如.
假设现在有个桶,高度为。
如图,我们把数顺序扔入桶中,数由于重力的作用,会沉到底部,当桶满后,扔入下一个桶。
由于每个数都至多出现次,数一定可以塞满个桶,所以塞完以后取次整行就一定能够互不重复。
显然,可以发现这样个数是能够被充分利用的,所以证毕。
代码:
#include<cstdio>
#include<cctype>
#include<cstring>
#include<algorithm>
#define R register
#define g getchar()
using namespace std;
typedef long long ll;
const int N=1e6+10;
void qr(int &x) {
char c=g;x=0;
while(!isdigit(c))c=g;
while(isdigit(c))x=x*10+c-'0',c=g;
}
void write(int x) {
if(x/10) write(x/10);
putchar(x%10+'0');
}
int n,cnt[N],ans,a[N];
ll c[N];
int main() {
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
qr(n);
for(int i=1,x;i<=n;i++) {
qr(x);c[++cnt[x]]++;
}
for(int i=2;i<=n;i++) c[i]+=c[i-1];
for(int i=n; i; i--) {
while(c[ans+1]>=(ll)(ans+1)*i)
ans++;
a[i]=ans;
}
for(int i=1;i<=n;i++) write(a[i]),puts("");
return 0;
}
B
思路来源
首先,很容易想到的暴力概率DP的做法。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5010,mod=998244353;
int f[N][N],inv[N<<1];
ll dfs(int n,int m) {//n<=m
if(f[n][m]!=-1) return f[n][m];
ll t=(ll)m*inv[n+m]%mod;int &ans=f[n][m];
if(n==m) ans=(t+dfs(n-1,n))%mod;
else ans=(t*(dfs(n,m-1)+1)%mod+((ll)n*inv[n+m]%mod)*dfs(n-1,m)%mod)%mod;
return ans;
}
int main() {
freopen("b.in","r",stdin);
freopen("b.out","w",stdout);
int n,m,s; scanf("%d %d",&n,&m);s=n+m;
inv[1]=1;for(int i=2;i<=s;i++) inv[i]=(ll)inv[mod%i]*(mod-mod/i)%mod;
if(n>m)swap(n,m);
memset(f,-1,sizeof f); f[0][0]=0;
printf("%lld
",dfs(n,m));return 0;
}
由这个暴力思路,我们可以进一步优化。
把整个状态空间抽象成一个的矩形(具体来讲,左上角为,右下角为.)
那么整个问题其实就是相当于一个从左上角走到右下角,途中只能向下或向右走的问题。
我们要仔细研究一下——这道题跟对角线有着密切的关系
(为什么,因为只用对角线上的选择倾向是不固定的——当两种题目的剩余数量相同时,显然猜中的概率为)
定义函数,表示从的期望猜中题数。
我们假设从,则
假设下一个经过点为,则有期望答对道题。
裂项相消,期望答对数
好像有点不太对劲,对——这种感觉没错。
因为对角线上的点(除原点),答对一道题的期望为,所以我们只要把所有路径经过对角线(除原点)的次数的期望求出来就行。
最终答案就是.
代码很短:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5e5+10,mod=998244353;
int jc[N<<1],inv[N<<1],n,m,s,ans;
ll power(ll a,ll b) {
ll c=1;
while(b) {
if(b&1) c=c*a%mod;
a=a*a%mod; b=b>>1;
}
return c;
}
int C(int n,int m) {
return (ll)jc[n]*inv[m]%mod*inv[n-m]%mod;
}
int main() {
freopen("b.in","r",stdin);
freopen("b.out","w",stdout);
scanf("%d %d",&n,&m); s=n+m; if(n>m) swap(n,m);
jc[0]=1; for(int i=1;i<=s;i++) jc[i]=(ll)jc[i-1]*i%mod;
inv[s]=power(jc[s],mod-2); for(int i=s; i; i--) inv[i-1]=(ll)inv[i]*i%mod;
ans=0;
for(int i=1;i<=n;i++) ans=(ans+(ll)C(2*i,i)*C(s-2*i,m-i)%mod)%mod;
ans=(ll)ans*power(C(s,n)<<1,mod-2)%mod;
printf("%d
",(ans+m)%mod);
return 0;
}
C
考场上一脸懵逼~
观察柿子,因为异或是二进制上的运算,所以我们考虑把进行拆分(谁想得到啊 )。
因为,所以我们把它分为两半,二进制下,每一段的位数为.
比方说,对于极限数据50000,我们把每一段定为8位。
定义块长为(没错,就是分块!)
设.
那么原柿子就转换为
因为为二的次幂,所以异或对前面运算结果的后位没有任何影响。
对于前位呢,在树上跑最大异或和即可。
算法思路:
预处理出让每个点充当上面的,对于所有的答案。
查询的时候,我们大段直接跳,边角暴力统计即可。
细节有点多,主要看代码:
#include<cmath>
#include<cstdio>
#include<cctype>
#include<cstring>
#include<algorithm>
#define g getchar()
using namespace std;
const int N=50010,T=260;
void qr(int &x) {
char c=g;x=0;
while(!isdigit(c))c=g;
while(isdigit(c))x=x*10+c-'0',c=g;
}
void write(int x) {
if(x/10) write(x/10);
putchar(x%10+'0');
}
int n,m,val[N],lg,t,mx[N][T],f[N][T];
int tot,trie[N<<3][2];//Trie
void ins(int x) {
int p=0;
for(int i=lg-1;i>=0;i--) {
int c=x>>i&1;
if(!trie[p][c]) trie[p][c]=++tot;
p=trie[p][c];
}
}
int ask(int x,int y) {//值为x,y为正在预处理的点
int p=0,ret=0,q=0;//p为Trie树上指针,ret为前lg位的最大异或值,q为与x异或的值
for(int i=lg-1;i>=0;i--) {
int c=x>>i&1;
if(trie[p][c^1]) ret|=1<<i,q|=(c^1)<<i,p=trie[p][c^1];
else q|=c<<i,p=trie[p][c];
}
return (ret<<lg)|mx[y][q];
}
struct edge {int y,next;}a[N<<1];int len,last[N];
void ins(int x,int y) {a[++len]=(edge){y,last[x]};last[x]=len;}
int fa[N],dep[N],top[N],vis[T];//fa为父亲,dep为深度,top为跳t次后的父亲
void cmax(int &x,int y) { x<y?x=y:0;}
void dfs(int x) {
if(dep[x]>=t) {
memset(trie,0,(tot+1)<<3); tot=0;//部分memset好
int y,d;
for(y=x,d=0;d<t;d++,y=fa[y]) {//d为深度差
cmax(mx[x][val[y]>>lg],(val[y]^d)&(t-1));//mx[i][j]表示以i为链底,前lg位为j的最大后lg位的值。
if(vis[val[y]>>lg]!=x)vis[val[y]>>lg]=x,ins(val[y]>>lg);//减少重复进Trie树
}
top[x]=y;
for(int i=0;i<t;i++) f[x][i]=ask(i,x);//预先记录,这样就不用浪费Trie树上的内存了——否则要记录很多东西
}
for(int k=last[x];k;k=a[k].next) {
int y=a[k].y;if(y==fa[x])continue;
dep[y]=dep[x]+1; fa[y]=x; dfs(y);
}
}
int main() {
// freopen("c.in","r",stdin);
// freopen("c.out","w",stdout);
qr(n); qr(m); lg=(log2(n)/2.0)+1; t=1<<lg;//lg这样算能保证t^2>=n-1
for(int i=1;i<=n;i++) qr(val[i]);
for(int i=1,x,y;i<n;i++)
qr(x),qr(y),ins(x,y),ins(y,x);
dep[1]=1;dfs(1);
while(m--) {
int x, y, ans=0, d=0;
for(qr(x),qr(y);dep[y]-dep[x]+1>=t;y=top[y],d++) cmax(ans,f[y][d]);//大段跳
for(d <<= lg;y!=fa[x];y=fa[y],d++) cmax(ans,val[y]^d);//局部暴力
write(ans); puts("");
}
return 0;
}