文章目录
- 考试时间及策略
- 考试结果
- 赛后总结
- 题解
- Balance Addicts
- Boboniu and String
- Bracket Insertion
- Conveyor
考试时间及策略
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 ∣s∣2n 和 特殊性质的 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=n−j+1n−l+1aq。那么 f i , j ← f k − 1 , l − 1 f_{i, j} \leftarrow f_{k - 1, l - 1} fi,j←fk−1,l−1。
仔细思考,我们会发现有一个要求是 [ k , i ] [k, i] [k,i] 与 [ n − j + 1 , n − l + 1 ] [n - j + 1, n - l + 1] [n−j+1,n−l+1] 的区间和相等。我们枚举 k k k 的时候显然没必要再把所有的 l ≤ j l \leq j l≤j 枚举一遍。我们可以开一个 桶 来存储信息优化复杂度。具体来说,我们设 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] [n−k+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} 2n−1。这个可以理解为我们可以在 n − 1 n - 1 n−1 个空位里面插板,可以不插,那么方案就是 2 n − 1 2^{n - 1} 2n−1。
#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();// 72ptselse if(f) solve2();// 12ptselse 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 fi∗∑l=1min(bi+1−bi,bk−bk−1)Cbi+1−bil×Cbk−bk−1l→fj。这个意思是我们可以在前面的 0 0 0 里面随便插板子,只要保证 b i + 1 b_{i + 1} bi+1 与 b i b_{i} bi 之间插的板子数量和 b k − 1 b_{k - 1} bk−1 与 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 1≤p1<p2<...<pk<qk<qk−1<qk−2<...<q1≤n。我们要求 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} 2r−l。
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 x≤x0+l
x ≥ x 0 − l x \geq x_0 - l x≥x0−l
y ≤ y 0 + l y \leq y_0 + l y≤y0+l
y ≥ y 0 − l y \geq y_0 - l y≥y0−l
x − y ≤ x 0 − y 0 + l x - y \leq x_0 - y_0 + l x−y≤x0−y0+l
x − y ≥ x 0 − y 0 − l x - y \geq x_0 - y_0 - l x−y≥x0−y0−l
我们维护答案点 x x x 的范围, y y y 的范围, x − y x - y x−y 的范围。最后枚举 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
题面
分析:
详见这里