lq的膜你赛

题面

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<=2105

对于 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<=5105n=m

对于 100 % 100\% 100%的数据: n , m < = 5 ∗ 1 0 5 n,m<=5*10^5 n,m<=5105

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 n1行描述了一棵树。每一行包含两个整数 x x x y y y ( 1 ≤ x , y ≤ n ) (1\leq x,y\leq n) (1x,yn),代表着树上的一条边

最后 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 n5104,q1.5105

T a s k 1 ( 20 % ) Task1(20\%) Task1(20%) n , q ≤ 5000 n,q\leq 5000 n,q5000

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(n2log(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 1cnt行的格子数就会增加1.最后做一个前缀和,就可以求出每条水平线下的格子数了。

重要引理

如果上述水平线下有 c c c个格子的话,那么能进行至少 t t t次操作,当且仅当 c ≥ t ∗ k c\ge t*k ctk

这个引理的必要性显然,我们只需要对其充分性进行证明。

证明:
物理是个好学科。
我们把水平线下的数按序排成一排,如 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 nm的矩形(具体来讲,左上角为 ( 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 vu道题。
裂项相消,期望答对数 = 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+vu+ua+ab+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)+jt(zdis(i,z)+1<t,dis(z,y)=jt,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)xorjt)=(aixordis(i,z))xorjt
因为 t t t为二的次幂,所以异或 j ∗ t j*t jt对前面运算结果的后 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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值