CF 1158 题解

A

设第 \(i\) 个女生的限制为 \(mx_i\),意思是存在一个男生给她 \(mx_i\) 的礼物。

设第 \(i\) 个男生的限制为 \(mn_i\),意思是要给所有女生 \(mn_i\) 的礼物。

设男女生都根据限制大小从小到大排序,首先如果 \(mx_n > b_1\) 那么就无解(所有女生至少收到了 \(mx_n\) 的礼物,但是第一个女生说她没收到,矛盾了)

对于每个女生,我们都要钦定好一个男生送给他礼物,我们肯定要找一个限制最宽的男生给女生送满足她限制的礼物(也就是 \(n\)),要注意如果 \(mx_n < mn_1\) 需要留一个位置来送 \(mx_n\) 的礼物来满足限制,这时候会贪心的让男 \(n\) 送给女 \(1\) 价值 \(mx_n\) 的礼物,再让男 \(n-1\) 送女 \(1\) 价值 \(mn_1\) 的礼物。

B

这题我只会找规律。为了方便找规律,先将 \(k\) 的定义改为最大的 \(k\) 满足 \([1 \ldots k]\) 的串要么没出现,要么出现 \(\geq 2\) 次。(也就是 \(k+1\) 时题目定义的实际答案)

观察这样的串:

10101010101010
100100100100100100
10001000100010001000
1000010000100001000010000

设它们的一个循环节长度为 \(d\),我们发现长度在 \(\leq 3d-1\) 时答案一直是 \(\leq d\),之后每增加一个长度答案就会 \(+1\),所以可以得到一个式子:\(3d-1+(k-d-1) = n \Rightarrow d = \frac{n-k+2}{2}\),而在题目的限制下又一定整除,就可以构造出来了。感觉完全看运气。。

C

相当于限定了一个数和一段区间的大小关系,线段树优化建图+拓扑排序即可。

D

也算是半个构造题?

先考虑没有规定拐点顺序怎么做:这是个经典问题。我们一开始选取最左边的点,每次走的点只要保证所有的点都在两点连线的一边就好了。

明白了这个就会有拐点顺序了:因为你选的“一边”可以是左边也可以是右边,根据题意调整即可。如果是 L 就选择最右边的点,R 就选择最左边的点。

E

神仙交互题。。

这个询问的本质是将每个点 \(i\) 距离自己 \(\leq d_i\) 的点都染黑(不包含自己)然后输出来哪些点被染黑了。

首先我们选取 \(1\) 号点为根,我们考虑过程分两步走:

求出每个点的深度

一个 naive 的做法是询问出所有距离 \(\leq i\) 的点,做个类似差分的东西就好了。

这个询问能传入长度为 \(n\) 的数组,我们只用一个未免太奢侈了,对于这种问题需要考虑如何合并询问。

我们如果维护处当前还未确定的深度区间 \([l_1,r_1],[l_2,r_2],\ldots,[l_m,r_m]\),对于每个区间我们只需要询问 \(mid-1,mid\) 就可以得出距离是 \(mid\) 的有哪些了。

如果所有询问直接扔进去一块做会怎么样?那么很有可能 \(mid-1\) 会超出边界,影响到上一个的选取,所以我们考虑排序后奇偶分组,这样就不会出现边界重复问题了,我们花了 \(4\) 次操作能将问题规模减小一般,这一部分操作是 \(4\lceil \log_2n \rceil\)

求出每个点的父亲

另一个 naive 的做法是对于一个深度为 \(d\) 的点 \(v\),我们枚举所有深度为 \(d-1\) 的点 \(u\),查询 \(u\) 到距离为 \(1\) 的点的集合,判断是否在里面。

我们先想想能不能优化到对每一层能一起判断:考虑如果选择了多个点其实相当于是能确定某个点的父亲是否在这个集合内,于是我们考虑逐位确定:假设现在考虑所有点父节点编号二进制位第 \(i\) 位是啥,我们把所有 \(d-1\) 层二进制第 \(i\) 位是 \(1\) 的扔进去查询就好了。

发现询问距离为 \(1\) 的询问会影响上面和下面一层,于是考虑按层 \(\bmod 3\) 分类,就不会有干扰的问题了。

F

你觉得我会神奇 \(O(\frac{n^3}{\log n})\)\(3500\) 计数题?大概省选的时候会补。

现在已经补了。

这种没见过的题就是要找性质然后对性质dp,具体过程是(判定性问题 $\to $ 状态和转移 $\to $ 优化)

判定性问题

如果判定一个串是否是 \(p\) 密度的呢?我们发现如果一个串是 \(p\) 密度的,首先 \(1 \ldots c\) 都要出现了,然后我们删掉 \(1\ldots c\) 这一段的所有的数,剩下的应该是一个 \(p-1\) 密度的串。

设计状态

有了这个东西就可以 dp 了:考虑设 \(f_{i,j}\) 表示强制最后一次选的是 \(i\)\(j\) 密度串的个数,转移的时候只需要预处理 \(g_{l,r}\) 表示区间 \([l,r]\) 的子序列数量,满足 \(1 \ldots c\) 只出现了一次,并且选择 \(a_r\) 后才满足 \(1 \ldots c\) 都出现了一次(否则会算重)。这个可以先枚举右端点,然后扫左端点,维护一个出现次数的桶,相当于限制 \(a_r\) 只能选一次,其他的只要至少选一个就行。时间复杂度 \(O(n^3)\)

优化

首先发现一个性质:\(p\) 密度串的长度至少是 \(cp\) 的。考虑判定过程每次至少删除 \(c\) 个字符可得。

那么发现 \(f_{i,j}\) 中,\(j \leq \frac{i}{c}\),复杂度降为了 \(O(\frac{n^3}{c})\)。但是还是不太行。

我们考虑复杂度瓶颈主要在枚举下一个段是什么,但是枚举下一个段是为了防止算重的,如果不想算重就只能记录下当前还没填完的段已经选过哪些数了。于是考虑设 \(f_{i,j,S}\) 表示考虑了前 \(i\) 个数,最后一次选的是 \(j\),当前这一个还没完整的段已经选过的数字集合是 \(S\),我们只要在某次转移 \(S=U\) 的时候直接将其强制转移到 \(S'=0\),就和上面的方法是等价的了,这样的复杂度是 \(O(\frac{n^2 2^c}{c})\),当 \(c=\log_2 n\) 的时候两式的最小值取到最大值 \(O(\frac{n^3}{\log_2 n})\) 。所以就当 \(c \leq \log_2n\) 的时候用方法二(状压),否则用方法一,总复杂度 \(O(\frac{n^3}{\log_2 n})\)(代码里是反着写的)

顺便一提时限 6s 我的代码是 5990ms 飘过的。。一些卡常技巧比如把能预先算的都放在最外层算有奇效。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 3000+5;
const int ha = 998244353;
int n,c,a[MAXN];

inline void add(int &x,int y){
	x += y;if(x >= ha) x -= ha;
}

int ans[MAXN];

inline int qpow(int a,int n=ha-2){
	int res = 1;
	while(n){
		if(n & 1) res = 1ll*res*a%ha;
		a = 1ll*a*a%ha;
		n >>= 1;
	}
	return res;
}

namespace Subtask1{ // O(n^2/c * 2^c)
	int f[2][MAXN][(1<<10)+3],now;
	// 考虑了前i个数 凑了j个[1..c],当前段哪些还没凑
	inline void Solve(){
		int U = (1<<c)-1;
		f[now=0][0][0] = 1;FOR(i,1,n) --a[i];
		FOR(i,1,n){
			FOR(j,0,(i-1)/c) FOR(S,0,U-1) f[now^1][j][S] = f[now][j][S];
			int t = (1<<a[i]);
			FOR(j,0,(i-1)/c){
				FOR(S,0,U-1){
					if(!f[now][j][S]) continue;
					int nS = S|t,nj = j;
					if(nS == U) nS = 0,nj = j+1;
					add(f[now^1][nj][nS],f[now][j][S]);
				}
			}
			now ^= 1;
		}
		FOR(S,0,(1<<c)-2) FOR(j,0,n/c) add(ans[j],f[now][j][S]);
		add(ans[0],ha-1);// 空
	}
}

namespace Subtask2{
	int f[MAXN][MAXN],g[MAXN][MAXN];
	/*
	f[i][j]: 最后一次选的是i,搞出了j个段
	转移需要预处理 g[i][j]: 最后一次选的是j,选出了一段 计算g只需要得到出现次数就行了
	*/
	int cnt[MAXN],now,sm;
	int pw[MAXN],inv[MAXN];
	
	inline void reset(){
		FOR(i,1,c) cnt[i] = 0;now = 0;sm = 1;
	}
	
	inline void add(int x){
		if(!cnt[x]){
			++cnt[x];++now;
			return;
		}
		sm = 1ll*sm*inv[cnt[x]]%ha;
		++cnt[x];
		sm = 1ll*sm*(pw[cnt[x]]+ha-1)%ha;
	}
	
	inline void Solve(){
		pw[0] = 1;FOR(i,1,MAXN-1) pw[i] = 2ll*pw[i-1]%ha;
		FOR(i,0,MAXN-1) inv[i] = qpow((pw[i]+ha-1)%ha);
		FOR(r,1,n){
			reset();
			ROF(l,r,1){
				add(a[l]);
				if(now != c) g[l][r] = 0;
				else g[l][r] = 1ll*sm*inv[cnt[a[r]]]%ha;
			}
		}
		f[0][0] = 1;
		FOR(i,0,n-1){
			FOR(j,0,i/c){
				if(!f[i][j]) continue;
				FOR(k,i+c,n){
					::add(f[k][j+1],1ll*f[i][j]*g[i+1][k]%ha);
				}
			}
		}
		// DEBUG(f[2][2]);
		// exit(0);
		FOR(j,0,n/c){
			reset();
			ROF(i,n,0){
				if(!f[i][j]){
					add(a[i]);continue;
				}
				int t=0;
				t = pw[n-i];
				if(now == c) ::add(t,ha-sm);
				::add(ans[j],1ll*f[i][j]*t%ha);
				add(a[i]);
			}
		}
		::add(ans[0],ha-1);
	}
}

int main(){
	scanf("%d%d",&n,&c);
	FOR(i,1,n) scanf("%d",a+i);
	if(c <= 10) Subtask1::Solve();
	else Subtask2::Solve();
	FOR(i,0,n) printf("%d ",ans[i]);puts("");
	return 0;
}
posted @ 2020-09-21 21:21  RainAir  阅读(71)  评论(0编辑  收藏  举报