11.8 33oj 模拟赛总结(时间安排 + 题解(数学 + 二分 + 括号匹配DP + 性质DP))

考试时间及策略

7:40 - 8:00       开题。T1 应该是个dp, 但是好像有点恶心。T2是个神秘构造。T3是个求随机括号匹配的概率,一眼应该是个 n 3 n^3 n3 的dp。T4有点离谱,不知道啥东西。
8:00 - 9:20       开始想T1, 发现部分分给的很足。 n 4 n^4 n4 的dp比较一眼,想了一会儿发现 可以优化到 n 3 n^3 n3。然后还有一个特殊性质 a i = 0 a_i = 0 ai=0 的分。这样加起来就有 84 p t s 84pts 84pts了。之后开始想怎样优化。想到了如果没有 0 0 0,那么状态可以优化到 O ( n ) O(n) O(n),转移是 O ( n 2 ) O(n^2) O(n2)。但是转移还是没办法通过,就放弃了接着思考这个思路。(实际上可以接着优化转移,优化过程并不是很难)。又想了 30 m i n 30min 30min,感觉转移真的没办法优化到 O ( n ) O(n) O(n),并且时间不是很够。就直接开写了。写完过了部分分的大样例。
9:25 - 10:00       想T2,发现了一些T2的性质,也成功把问题转化成了图上找一点到其它点距离的最大值最小。但是接着就不会了。只能去拿 ∣ s ∣ 2 n |s|^2n s2n 和 特殊性质的 45 45 45 分了。
10:00 - 11:20       想 T3,想到了一些自认为重要的性质,并且想到了一个看似很对的 O ( N 3 ) O(N^3) O(N3) 的dp。开写!!!写完之后发现过不去样例。调着调着发现刚才想的好像不对。深入思考感觉给状态换一个定义就好了。然后改了改,发现过掉了第二个样例,但是第三个样例过不去了。一直在想哪里错了,但是想不出来。后来发现时间只剩了20min,但是我T4还没开!!!赶紧把T3的几个我会写的特殊性质分写了,含泪拿了 20 p t s 20pts 20pts。去看T4了。
11:20 - 11:40       5min理解完题意,发现35pts是可以拿的,火速开写。写完没过样例!!!慌了,静态查错。终于在时间还剩 1min 的时候过样例了。赶紧交了。

考试结果

期望得分:84 + 45 + 20 + 35 = 184
实际得分:84 + 45 + 20 + 0 = 149
暴力哥都没当成功

赛后总结

T1:应该是有能力场A的,但是没有深入往下想。
T2: 差了一步,正解就是接着往下做了个二分。但是我不是很会线性规划,没做出来也正常。
T3:假完了,白白浪费一个多小时。
T4:漏了一句话导致没拿到35pts,亏麻了。但主要还是因为没有充分的时间去写。下次应该先尽快把暴力写完再去磕正解。

这一场是搬的CF的题,第一题2300,后三题都是2600+,有点恐怖

题解

Balance Addicts

题面

简要题意:
       给定一个长度为 n n n 的序列 a a a,对于每一种将 a a a 划分成若干个子串的方式,我们设当前划分有 k k k 个子串,分别描述为 ( l 1 , r 1 ) , ( l 2 , r 2 ) , . . . ( l k , r k ) (l_1, r_1),(l_2, r_2),...(l_k, r_k) (l1,r1),(l2,r2),...(lk,rk),定义长度为 k k k 的数组 s s s 满足 s i = ∑ j = l i r i a j s_i = \sum_{j = l_i}^{r_i}a_j si=j=liriaj。如果 s s s 是一个回文数组,那么称这一种划分方式是好的。求 a a a 有多少种好的划分。

分析:
       下面提供两种解法。

       S o l 1 Sol_1 Sol1

       因为要求最后数组是回文的,所以我们可以想到一个 O ( n 4 ) O(n^4) O(n4) 的dp:设 f i , j f_{i, j} fi,j 表示序列的前 i i i 个 与后 j j j 个进行分段,形成 对称 的两部分的划分方案数。那么我们枚举左右两边的段的断点 k k k l l l。使得 ∑ p = k i a p = ∑ q = n − j + 1 n − l + 1 a q \sum_{p = k}^{i}a_p = \sum_{q = n - j + 1}^{n - l + 1}a_q p=kiap=q=nj+1nl+1aq。那么 f i , j ← f k − 1 , l − 1 f_{i, j} \leftarrow f_{k - 1, l - 1} fi,jfk1,l1

       仔细思考,我们会发现有一个要求是 [ k , i ] [k, i] [k,i] [ n − j + 1 , n − l + 1 ] [n - j + 1, n - l + 1] [nj+1,nl+1] 的区间和相等。我们枚举 k k k 的时候显然没必要再把所有的 l ≤ j l \leq j lj 枚举一遍。我们可以开一个 来存储信息优化复杂度。具体来说,我们设 c n t i , j cnt_{i, j} cnti,j 表示所有 f i , k f_{i, k} fi,k 的和, 其中需要满足 区间 [ n − k + 1 , n ] [n - k + 1, n] [nk+1,n] 的和为 j j j。然后我们枚举 k k k,转移时直接调用桶里的值就好了。因为 j j j 可能很大,但是后缀和的取值只有 n n n 种,离散化一下就好了。有一些细节:比如需要倒序枚举 j j j
       对于 a i = 0 a_i = 0 ai=0,显然答案是 2 n − 1 2^{n - 1} 2n1。这个可以理解为我们可以在 n − 1 n - 1 n1 个空位里面插板,可以不插,那么方案就是 2 n − 1 2^{n - 1} 2n1

#include<bits/stdc++.h>// 84pts
using namespace std;
const int N = 2e5 + 10;
const int M = 5e2 + 100;
typedef long long LL;
const LL mod = 998244353;
int n, tot, rk[N];
LL dp[M][M], a[N], cnt[M][M], sum[N], b[N];
LL S[N];
int Find(LL x){
	int c = lower_bound(b + 1, b + tot + 1, x) - (b);
	if(c > tot || (b[c] != x)) return -1;
	return c;
}
void solve1(){
	for(int i = 1; i <= n; i++){
		sum[i] = sum[i - 1] + a[i];
	}
	S[n + 1] = 0;
	b[++tot] = S[n + 1];
	for(int i = n; i >= 1; i--){
		S[i] = S[i + 1] + a[i];
		b[++tot] = S[i];
	}
	sort(b + 1, b + tot + 1);
	tot = unique(b + 1, b + tot + 1) - (b + 1);
	for(int i = n; i >= 1; i--){
		rk[i] = lower_bound(b + 1, b + tot + 1, S[i]) - (b);
	}
	dp[0][n + 1] = 1LL;
	cnt[0][1] = 1LL;
	for(int j = n; j >= 1; j--){// 倒序做 
    	for(int i = j - 1; i >= 1; i--){
			for(int k = i - 1; k >= 0; k--){
                int p = Find(S[j] - sum[i] + sum[k]);
                if(p != -1) dp[i][j] = (dp[i][j] + cnt[k][p]) % mod;
			}
			cnt[i][rk[j]] = (cnt[i][rk[j]] + dp[i][j]) % mod;
		}
	}
	LL res = 0;
	for(int i = 1; i < n; i++){
		res = (res + dp[i][i + 1]) % mod;
	}
	for(int i = 1; i <= n; i++){
		for(int j = i; j <= n; j++){
			res = (res + dp[i - 1][j + 1]) % mod;
		}
	}
	printf("%lld\n", res % mod);
}
LL Pow(LL x, LL y){
	LL res = 1, k = x % mod;
	while(y){
		if(y & 1) res = (res * k) % mod;
		y >>= 1;
		k = (k * k) % mod;
	}
	return res % mod;
}
void solve2(){//插板法 
	printf("%lld\n", Pow(2LL, 1LL * n - 1) % mod);
}
void solve3(){
	
}
int main(){
	scanf("%d", &n);
	bool f = 1;
	for(int i = 1; i <= n; i++){
		scanf("%lld", &a[i]);
		if(a[i]) f = 0;
	} 
	if(n <= 500) solve1();// 72pts
	else if(f) solve2();// 12pts
	else solve3();// 不会 
	return 0;
}

       接着我们来接着优化。不难发现,如果序列里面没有 0 0 0,那么 f i , j f_{i, j} fi,j 的每一个 i i i 都唯一对应一个 j j j,所以可以把 j j j 哪一维省去。但是如果序列里面有 0 0 0 呢?我们只处理非 0 0 0 的位置, 考虑用 0 0 0 的数量去改变转移。
       假设 b b b 序列是 a a a 序列去除序列里面的 0 0 0 后剩下元素的 位置 序列。设 f i f_{i} fi 表示 b i b_i bi 它对应后缀的 d p dp dp 值。那么考虑朴素转移:假设我们知道 f i f_i fi,向后枚举一个 j j j,转移给 f j f_j fj。设 b i b_i bi 对应的是 b k b_k bk。那么转移就是 f i ∗ ∑ l = 1 m i n ( b i + 1 − b i , b k − b k − 1 ) C b i + 1 − b i l × C b k − b k − 1 l → f j f_{i} * \sum_{l = 1}^{min(b_{i + 1} - b_{i}, b_{k} - b_{k - 1})} C_{b_{i + 1} - b_{i}}^{l} \times C_{b_{k} - b_{k - 1}}^{l} \to f_j fil=1min(bi+1bi,bkbk1)Cbi+1bil×Cbkbk1lfj。这个意思是我们可以在前面的 0 0 0 里面随便插板子,只要保证 b i + 1 b_{i + 1} bi+1 b i b_{i} bi 之间插的板子数量和 b k − 1 b_{k - 1} bk1 b k b_{k} bk 相等就行,但是不能不插。这样转移是 O ( n ) O(n) O(n) 的。我们接着考虑实际上对每一个 j > i j > i j>i f i f_i fi 的贡献都是一样的,我们可以维护一个变量 A d d Add Add,表示前面对当前及后面 f f f 数组的贡献之和。然后每次用 A d d Add Add 更新 f i f_{i} fi,并把 f i f_{i} fi 贡献到 A d d Add Add 里面就好了。时间复杂度 O ( n ) O(n) O(n)

       放一个 klz 的

#include<bits/stdc++.h>
using namespace std; 

typedef long long LL ;
const int N = 2e5+10 ;
const LL mod = 998244353 ;

int n , a[N] , pos[N] , len ;
LL S[N] , ans ; 
LL ksm( LL a , LL b)
{
	LL res = 1 , t = a % mod ;
	while( b ) {
		if( b&1 ) res = res * t % mod ;
		b = b >> 1 ;
		t = t * t % mod ;
	}
	return res ;
}
 
namespace part2
{
	int ret[N] ; LL f[N] , fac[N] , inv[N] ;
	void pre_work()
	{
		fac[0] = 1 ;
		for(int i = 1 ; i <= n ; i ++ ) fac[i] = fac[i-1] * i % mod ;
		inv[n] = ksm( fac[n] , mod-2 ) ;
		for(int i = n-1 ; i >= 1 ; i -- ) inv[i] = inv[i+1] * (i+1) % mod ;
		inv[0] = 1 ;
	}
	inline LL C ( int n , int m ) 
	{
		return fac[n] * inv[m] % mod * inv[n-m] % mod ;
	}
	void solve()
	{
		pre_work() ;
		for(int i = 1 ; i <= len ; i ++ ) {
			S[i] = S[i-1] + a[pos[i]] ;
		}
		for(int i = 0 , j = len+1 ; i < j ; i ++ ) {
			while( i < j && S[i]>S[len]-S[j-1] ) j -- ;
			if( i < j && S[i] == S[len]-S[j-1] ) {
				ret[i] = j ;
			}
		}
		f[0] = 1 ;  pos[0] = 1 , pos[len+1] = n ;
		LL ADD = 0 ;
		for(int i = 0 ; i <= len ; i ++ ) {
			if( !ret[i] ) continue ;
			f[i] = ( f[i] + ADD ) % mod ;
			int dl = pos[i+1]-pos[i] , dr = pos[ret[i]]-pos[ret[i]-1] ; LL c = 0 ;
			for(int k = 1 ; k <= min(dl,dr) ; k ++ ) {
				c = ( c + C(dl,k)*C(dr,k)%mod ) % mod ;
			}
			ADD = ( ADD + f[i]*c%mod + (!i) ) % mod ;
			if( i+1 != ret[i] ) {
				ans = ( ans + f[i]*c % mod ) % mod ;
			}
			else {
				ans = ( ans + f[i]*(ksm(2,dl)-1)%mod + mod ) % mod ;
			}
			if( !i ) ans ++ ; // 补上整体选的 
		}
		printf("%lld\n" , ans ) ;
	}
}

int main() 
{
	scanf("%d" , &n );
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &a[i] ) ;
		if( a[i] ) pos[++len] = i ;
	}
	part2::solve() ;
	return 0 ; 
}

       S o l 2 Sol_2 Sol2
       既然要求最后是回文数组,我们需要从两边一刀刀往内切分。我们考虑把一种划分方式 双射 成两个长度分别为 k k k 的序列 p p p q q q。满足:

       1 ≤ p 1 < p 2 < . . . < p k < q k < q k − 1 < q k − 2 < . . . < q 1 ≤ n 1 \leq p_1 < p_2 < ... < p_k < q_k < q_{k - 1} < q_{k - 2} < ... < q_{1} \leq n 1p1<p2<...<pk<qk<qk1<qk2<...<q1n。我们要求 p r e p i = s u f q i pre_{p_i} = suf_{q_i} prepi=sufqi。其中 p r e pre pre s u f suf suf 分别表示原序列的 前缀和 以及 后缀和

       首先对于任意一种序列都可以映射一种划分方式。其次,对于任意一种划分方式,如果是奇回文,那我们可以从两边一次依次把 段的端点 拿出来构成序列。如果是偶回文,那么我们让中间段的右端点为 p k p_k pk,右端点加一的位置为 q k q_k qk 就好了。

       知道了双射的性质,那么现在我们只需要求这样的 序列数量 就好了。因为序列里面的元素都是正数,所以从左往右前缀和不降。从右往左后缀和不降,所以我们可以把序列按照前缀和的值划分成一段一段的,同时也可以把序列按照后缀和划分成一段一段的。设前缀和为 s u m sum sum 的段长度为 l 1 l_1 l1,后缀和为 s u m sum sum 的段的长度为 l 2 l_2 l2。如果这两段不想交,那么在和为 s u m sum sum 的方案是 ∑ i = 0 m i n ( l 1 , l 2 ) C l 1 i × C l 2 , i \sum_{i = 0}^{min(l1, l2)}C_{l1}^{i} \times C_{l2, i} i=0min(l1,l2)Cl1i×Cl2,i。否则,如果相交,我们能发现这两段会公共覆盖不是端点的中间段,这样答案是 2 r − l 2^{r - l} 2rl

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
typedef long long LL;
const LL mod = 998244353;
int n, T;
LL a[N], pre[N], suf[N], fac[N], inv[N];
LL C(int n, int m){return fac[n] * inv[m] % mod * inv[n - m] % mod;}
LL Pow(LL x, LL y){
	LL res = 1, k = x % mod;
	while(y){
		if(y & 1) res = res * k % mod;
		y >>= 1;
		k = k * k % mod;
	}
	return res;
}
void solve(){
	scanf("%d", &n);
	pre[0] = suf[n + 1] = 0;
	for(int i = 1; i <= n; i++){
		scanf("%lld", &a[i]);
		pre[i] = pre[i - 1] + a[i];
	}
	for(int i = n; i >= 1; i--){
		suf[i] = suf[i + 1] + a[i];
	}
	LL res = 1;
	int l = 1, r = n;
	while(l < r){
		if(pre[l] < suf[r]){l++; continue;}
		if(suf[r] < pre[l]){r--; continue;}
		int rt = l, lt = r;
		while(rt < n && pre[rt + 1] == pre[l]) rt++;
		while(lt && suf[lt - 1] == suf[r]) lt--;
		if(rt == r - 1 && lt == l + 1){
			res = res * Pow(2LL, 1LL * r - l) % mod;
			break;
		}
		if(rt == n || lt == 1){
	        res = res * Pow(2LL, 1LL * n - 1) % mod;
			break;
		}
		else{
			LL o = 0;
			for(int k = 0; k <= min(rt - l + 1, r - lt + 1); k++){
				o = (o + C(rt - l + 1, k) * C(r - lt + 1, k) % mod) % mod;
			}
			res = res * o % mod;
		}
		l = rt + 1; r = lt - 1;
	}
	printf("%lld\n", res);
}
int main(){
	fac[0] = 1LL;
	for(int i = 1; i < N; i++) fac[i] = fac[i - 1] * (1LL * i) % mod;
	inv[N - 1] = Pow(fac[N - 1], mod - 2LL) % mod;
	for(int i = N - 2; i >= 0; i--) inv[i] = inv[i + 1] * (1LL * i + 1) % mod;
	scanf("%d", &T);
	while(T--) solve();
	return 0;
}

Boboniu and String

题面

简要题意:
有点长,自己看吧

分析:

       我们考虑给的条件是什么意思。
       两个字符串“相似”的定义:两个字符串 N N N B B B 的数量分别相等。
       操作:一次删除或者增加 一个 N N N 或者一个 B B B。或者同时增加或删除一个 N N N,一个 B B B
       要求我们求出一个串 T T T,使得 T T T 变成与给定的所以 S i S_i Si 相似需要的最大操作数最小。

       发现了相似只与 数量 有关,与 顺序 无关,那么这道题就好做多了。我们把 N N N 的数量看做 横坐标,把 B B B 的数量看做 纵坐标,那么问题就转化成了:在平面上有若干个点。你需要求出一个点,满足它到所有给定点的移动步数最大的最小。其中移动方式有 6 6 6 种。
在这里插入图片描述
       有 最大的最小,我们肯定是要二分的。设二分的答案是 l l l,那么所有给定点都有一个对应区域表示区域内的点能够在 l l l 步内到达它,我们判断所有区域有没有公共部分就好了。所有点的区域可以用 线性规划 表示。设当前点坐标是 ( x 0 , y 0 ) (x_0, y_0) (x0,y0),那么对于一个能够在 l l l 步内到达它的点 ( x , y ) (x, y) (x,y) 而言,需要满足一下不等关系:

x ≤ x 0 + l x \leq x_0 + l xx0+l
x ≥ x 0 − l x \geq x_0 - l xx0l
y ≤ y 0 + l y \leq y_0 + l yy0+l
y ≥ y 0 − l y \geq y_0 - l yy0l
x − y ≤ x 0 − y 0 + l x - y \leq x_0 - y_0 + l xyx0y0+l
x − y ≥ x 0 − y 0 − l x - y \geq x_0 - y_0 - l xyx0y0l

       我们维护答案点 x x x 的范围, y y y 的范围, x − y x - y xy 的范围。最后枚举 x x x 的范围内的数,检验存不存在合法解就好了。

       时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
const int M = 1e6 + 10;
typedef pair< int, int > PII;
int n, x[N], y[N], lx, rx, ly, ry, lz, rz, resb, resn;// 分别是 x 的限制, y的限制, 和 x - y 的限制 
char str[M];
PII check(int l){
	lx = ly = 0;
	lz = -1e8;
	rx = ry = rz = 1e8;
	for(int i = 1; i <= n; i++){
		lx = max(lx, x[i] - l);
		rx = min(rx, x[i] + l);
		ly = max(ly, y[i] - l);
		ry = min(ry, y[i] + l);
		lz = max(lz, x[i] - y[i] - l);
		rz = min(rz, x[i] - y[i] + l);
	}
    if(rx <= 0 && ry <= 0) return make_pair(-1, -1);
	for(int i = max(lx, 0); i <= rx; i++){
		int tl = i - rz, tr = i - lz;
		tl = max(tl, ly); tr = min(tr, ry);
		if(tl <= tr){
			if(tr > 0 || i > 0) return make_pair(i, tr);
		}
	}
	return make_pair(-1, -1);
}
int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i++){
		scanf("%s", str + 1);
		int len = strlen(str + 1);
		for(int j = 1; j <= len; j++){
			if(str[j] == 'B') x[i]++;
			else y[i]++;
		}
	}
	int l = 0, r = 2e6 + 10, mid, res = -1;
	while(l <= r){
		mid = (l + r >> 1);
		PII k = check(mid);
		if(k != make_pair(-1, -1)){
			res = mid;
			resb = k.first, resn = k.second;
			r = mid - 1;
		}
		else l = mid + 1;
	}
	printf("%d\n", res);
	for(int i = 1; i <= resb; i++) putchar('B');
	for(int i = 1; i <= resn; i++) putchar('N');
	return 0;
}

Bracket Insertion

题面

分析:
       详见这里

Conveyor

题面

分析:
       详见这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值