文章目录
- 考试时间及策略
- 考试结果
- 赛后总结
- 题解
- 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();// 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
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
题面
分析:
详见这里