题面
CSP-S 2019膜你赛
liqing
A
(a.cpp/in/out,1s,128MiB)
l x j lxj lxj因为是一个立青,所以他对所有的知识一窍不通。
一天,他遇到了一道题,但他肯定不会啊,所以作为神犇的你能帮帮他吗?
给你一个 n n n个数的序列。 l x j lxj lxj会选择一个数 K K K,然后对这个序列进行若干次操作。一次操作是在这个序列中选择恰好 K K K个互不相同的数,并将它们从序列中删掉(之后就不能再选了)。
由于 l x j lxj lxj很无聊,所以他想知道对于每个 K = 1 , 2 , 3 , . . . , n K=1,2,3,...,n K=1,2,3,...,n,他最多能进行多少次操作。
输入格式
第一行仅一个正整数 n n n
第二行给出一个长度为 n n n的序列,保证其中每个整数 ∈ [ 1 , n ] \in[1,n] ∈[1,n]
输出格式
有 n n n行,每一行仅一个整数。第 i i i行的整数代表着当 K = i K=i K=i时的答案
样例输入
3
2 1 2
样例输出
3
1
0
数据范围与约定
对于 20 % 20\% 20%的数据: n < = 10 n<=10 n<=10
对于 50 % 50\% 50%的数据: n < = 2000 n<=2000 n<=2000
对于 70 % 70\% 70%的数据: n < = 2 ∗ 1 0 5 n<=2*10^5 n<=2∗105
对于 100 % 100\% 100%的数据: n < = 1 0 6 n<=10^6 n<=106
B
(b.cpp/in/out,1s,128MiB)
l x j lxj lxj并没有听懂你上一题的解法,所以就自闭了。于是他想找些谜语来快乐一下,你听到他的要求后就给了他 n + m n+m n+m道谜语,这些谜语的答案要不是 y e s yes yes,要不是 n o no no。
但是由于 l x j lxj lxj是个立青,所以他一题都不会做,只能瞎猜。。。你看他太过可怜,就告诉了他其中有 n n n道题目答案是 y e s yes yes, m m m道题目答案是 n o no no,并且每当 l x j lxj lxj回答完一道问题之后你就会马上告诉他这道题的正确答案。
现在 l x j lxj lxj想通过你的帮助来最大化他答对题目的数目,你能告诉他如果按最优策略的话他答对题目数目的期望值吗?
输入格式
仅一行两个正整数 n n n和 m m m。
输出格式
仅一行一个整数,表示期望值对 998244353 998244353 998244353取模后的结果
样例输入
1 1
样例输出
499122178
提示
样例解释:
第一个问题 l x j lxj lxj有 50 % 50\% 50%的概率答对。而当他打完后,由于他已经知道了第一个问题的正确答案了,并且只有两个问题,所以第二个问题他有 100 % 100\% 100%的概率答对。
所以输出的真实值是 1 2 + 1 = 3 2 \frac{1}{2}+1=\frac{3}{2} 21+1=23。
数据范围与约定
对于 20 % 20\% 20%的数据: n + m < = 10 n+m<=10 n+m<=10
对于 50 % 50\% 50%的数据: n , m < = 5000 n,m<=5000 n,m<=5000
对于 60 % 60\% 60%的数据: m i n ( n , m ) < = 5000 min(n,m)<=5000 min(n,m)<=5000
另有 20 % 20\% 20%的数据: n , m < = 5 ∗ 1 0 5 , n = m n,m<=5*10^5,n=m n,m<=5∗105,n=m
对于 100 % 100\% 100%的数据: n , m < = 5 ∗ 1 0 5 n,m<=5*10^5 n,m<=5∗105
C
(c.cpp/in/out,2s,256MiB)
l x j lxj lxj因为是一个立青,所以他对所有的知识一窍不通。
一天,他遇到了一道题,但他肯定不会啊,所以作为神犇的你能帮帮他吗?
给你一棵以 1 1 1号节点为根的树,每个点有一个点权 a i a_i ai。
有 q q q次询问,每次询问给出两个数 x x x和 y y y,要你求 x x x到 y y y路径上最大的 a i x o r d i s ( i , y ) a_i\ xor\ dis(i,y) ai xor dis(i,y),保证 x x x是 y y y的祖先。( x o r xor xor是异或的意思, d i s ( a , b ) dis(a,b) dis(a,b)是 a a a到 b b b简单路径的边数)
输入格式
第一行包含两个正整数 n n n和 q q q,分别代表了这棵树的点数和总询问次数
第二行有 n n n个整数,第 i i i个整数代表着 a i a_i ai。保证其中每个整数 ∈ [ 0 , n ] \in[0,n] ∈[0,n]
接下来 n − 1 n-1 n−1行描述了一棵树。每一行包含两个整数 x x x和 y y y ( 1 ≤ x , y ≤ n ) (1\leq x,y\leq n) (1≤x,y≤n),代表着树上的一条边
最后 q q q行,每一行包含两个整数 x x x和 y y y ( 1 < = x , y < = n ) (1<=x,y<=n) (1<=x,y<=n),代表着一次询问。保证 x x x是 y y y的祖先
输出格式
有 q q q行,第 i i i行代表着第 i i i次询问的答案
样例输入
5 3
0 3 2 1 4
1 2
2 3
3 4
3 5
1 4
1 5
2 4
样例输出
3
4
3
数据范围与约定
对于 100 % 100\% 100%的数据: n ≤ 5 ∗ 1 0 4 , q ≤ 1.5 ∗ 1 0 5 n\leq 5*10^4,q\leq 1.5*10^5 n≤5∗104,q≤1.5∗105
T a s k 1 ( 20 % ) Task1(20\%) Task1(20%): n , q ≤ 5000 n,q\leq 5000 n,q≤5000
T a s k 2 ( 30 % ) Task2(30\%) Task2(30%):保证 a i a_i ai是 2 2 2的整数次幂
T a s k 3 ( 10 % ) Task3(10\%) Task3(10%):保证图是一条链
T a s k 4 ( 20 % ) Task4(20\%) Task4(20%):无
T a s k 5 ( 20 % ) Task5(20\%) Task5(20%):保证所有的 a i a_i ai都相同
解题报告
(爆零记)
A
这道题我在考场上一眼就看出来是一道贪心题,可是考场上始终想不到正解。
最终迫不得已,写了一个
O
(
n
2
∗
log
(
n
)
)
O(n^2*\log(n))
O(n2∗log(n))的做法,先贴一下吧——真的很好写。
#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;
}
正解——果然贪心。
显然,只有每个数的出现次数有效(并不在意数是多少)
设
a
n
s
k
ans_k
ansk为第k个答案(输出值),则容易发现
a
n
s
k
>
=
a
n
s
k
+
1
ans_k>=ans_{k+1}
ansk>=ansk+1
(单次取更多数的话,能进行的次数不会更多)
所以我们可以倒着求答案,因为倒着来答案非降,所以如果可以在 O ( 1 ) O(1) O(1)判断一个数可不可以成为答案的话,就可以 O ( n ) O(n) O(n)求出所有答案了。
本题的重点在于如何判断对于一个确定的
k
k
k,是否能执行
t
t
t次操作。
我们现在需要统计一下这条水平线下有多少个有用的单位格子(1*1)(表示每一个数)
怎么算呢?——如果一个数出现次数为 c n t cnt cnt,那么 1 ∼ c n t 1\sim cnt 1∼cnt行的格子数就会增加1.最后做一个前缀和,就可以求出每条水平线下的格子数了。
重要引理
如果上述水平线下有 c c c个格子的话,那么能进行至少 t t t次操作,当且仅当 c ≥ t ∗ k c\ge t*k c≥t∗k
这个引理的必要性显然,我们只需要对其充分性进行证明。
证明:
物理是个好学科。
我们把水平线下的数按序排成一排,如
1
,
1
,
2
,
3
,
3
1,1,2,3,3
1,1,2,3,3.
假设现在有
k
k
k个桶,高度为
t
t
t。
如图
t
=
3
t=3
t=3,我们把数顺序扔入桶中,数由于重力的作用,会沉到底部,当桶满后,扔入下一个桶。
由于每个数都至多出现
t
t
t次,数一定可以塞满
k
k
k个桶,所以塞完以后取
t
t
t次整行就一定能够互不重复。
显然,可以发现这样 c c c个数是能够被充分利用的,所以证毕。
代码:
#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
思路来源
首先,很容易想到
O
(
n
2
)
O(n^2)
O(n2)的暴力概率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\n",dfs(n,m));return 0;
}
由这个暴力思路,我们可以进一步优化。
把整个状态空间抽象成一个
n
∗
m
n*m
n∗m的矩形(具体来讲,左上角为
(
n
,
m
)
(n,m)
(n,m),右下角为
(
0
,
0
)
(0,0)
(0,0).)
那么整个问题其实就是相当于一个从左上角走到右下角,途中只能向下或向右走的问题。
我们要仔细研究一下——这道题跟对角线
y
=
x
y=x
y=x有着密切的关系
(为什么,因为只用对角线上的选择倾向是不固定的——当两种题目的剩余数量相同时,显然猜中的概率为
1
2
\dfrac{1}{2}
21)
定义函数
f
(
n
,
m
,
v
)
=
m
a
x
(
n
,
m
)
−
v
f(n,m,v)=max(n,m)-v
f(n,m,v)=max(n,m)−v,表示从
(
n
,
m
)
走
到
(
v
,
v
)
(n,m)走到(v,v)
(n,m)走到(v,v)的期望猜中题数。
我们假设从
(
n
,
m
)
走
到
的
第
一
个
对
角
线
上
的
点
为
(
v
,
v
)
(n,m)走到的第一个对角线上的点为(v,v)
(n,m)走到的第一个对角线上的点为(v,v),则
期
望
答
对
f
(
n
,
m
,
v
)
道
题
期望答对f(n,m,v)道题
期望答对f(n,m,v)道题
假设下一个经过点为
(
u
,
u
)
(u,u)
(u,u),则有期望答对
v
−
u
v-u
v−u道题。
裂项相消,期望答对数
=
m
a
x
(
n
,
m
)
−
v
+
v
−
u
+
u
−
a
+
a
−
b
+
b
(
其
他
对
角
线
上
的
点
)
…
…
−
0
=
m
a
x
(
n
,
m
)
−
0
=max(n,m)-v+v-u+u-a+a-b+b(其他对角线上的点)……-0=max(n,m)-0
=max(n,m)−v+v−u+u−a+a−b+b(其他对角线上的点)……−0=max(n,m)−0
好像有点不太对劲,对——这种感觉没错。
因为对角线上的点(除原点),答对一道题的期望为
1
2
\dfrac{1}{2}
21,所以我们只要把所有路径经过对角线(除原点)的次数的期望
w
w
w求出来就行。
最终答案就是 m a x ( n , m ) + 1 2 w max(n,m)+\frac{1}{2}w max(n,m)+21w.
代码很短:
#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\n",(ans+m)%mod);
return 0;
}
C
考场上一脸懵逼~
观察柿子
a
i
xor
d
i
s
(
i
,
y
)
a_i \operatorname{xor} dis(i,y)
aixordis(i,y),因为异或是二进制上的运算,所以我们考虑把
d
i
s
(
i
,
y
)
dis(i,y)
dis(i,y)进行拆分(谁想得到啊 )。
因为
d
i
s
(
i
,
y
)
∈
[
0
,
n
)
dis(i,y)\in [0,n)
dis(i,y)∈[0,n),所以我们把它分为两半,二进制下,每一段的位数
l
g
lg
lg为
⌊
log
(
n
)
⌋
+
1
\lfloor\log(n)\rfloor+1
⌊log(n)⌋+1.
比方说,对于极限数据50000,我们把每一段定为8位。
定义块长
t
t
t为
2
l
g
2^{lg}
2lg(没错,就是分块!)
设
d
i
s
(
i
,
y
)
=
d
i
s
(
i
,
z
)
+
j
∗
t
(
z
是
树
上
路
径
上
的
一
个
点
,
d
i
s
(
i
,
z
)
+
1
<
t
,
d
i
s
(
z
,
y
)
=
j
∗
t
,
j
∈
[
0
,
t
)
)
dis(i,y)=dis(i,z)+j*t(z是树上路径上的一个点,dis(i,z)+1<t,dis(z,y)=j*t,j\in [0,t) )
dis(i,y)=dis(i,z)+j∗t(z是树上路径上的一个点,dis(i,z)+1<t,dis(z,y)=j∗t,j∈[0,t)).
那么原柿子就转换为
a
i
xor
(
d
i
s
(
i
,
z
)
xor
j
∗
t
)
=
(
a
i
xor
d
i
s
(
i
,
z
)
)
xor
j
∗
t
a_i \operatorname{xor} (dis(i,z) \operatorname{xor} j*t)=(a_i \operatorname{xor} dis(i,z)) \operatorname{xor} j*t
aixor(dis(i,z)xorj∗t)=(aixordis(i,z))xorj∗t
因为
t
t
t为二的次幂,所以异或
j
∗
t
j*t
j∗t对前面运算结果的后
l
g
lg
lg位没有任何影响。
对于前
l
g
lg
lg位呢,在
T
r
i
e
Trie
Trie树上跑最大异或和即可。
算法思路:
预处理出让每个点充当上面的
z
z
z,对于所有
j
∈
[
0
,
t
)
j\in[0,t)
j∈[0,t)的答案。
查询的时候,我们大段直接跳,边角暴力统计即可。
细节有点多,主要看代码:
#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;
}