CF1416E Split
CF1416E
给定长度为 \(n\) 的序列 \(a\),构造长度为 \(2n\) 的正整数序列 \(b\),满足:
- \(a_i=b_{2i-1}+b_{2i}\)
对于我们得到的序列 \(b\),我们进行一次压缩,即将相邻的相同数字进行合并,设 \(|b|\) 表示合并后序列 \(b\) 的大小。
给定序列 \(a\),你需要求 \(|b|\) 的最小值。
\(\sum n\le 5\cdot 10^5,2\le a_i\le 10^9\)
Solution
我们发现 \(b\) 序列上每两个元素将对应 \(a\) 上一个元素。
于是我们将问题当作将 \(a\) 拆成两个元素 \(c_i,d_i\),然后贡献初始答案为 \(2n\)
-
如果 \(c_i=d_i\),那么贡献 \(-1\)
-
如果 \(d_i=c_{i+1}\),那么贡献 \(-1\)
最小化贡献。
也相当于最大化 \(c_i=d_i,d_i=c_{i+1}\) 的对子数量,方便起见我们认为目标是最大化贡献。
我们称 \(c_i=d_i\) 为一次拆分,贡献为 \(1\)
我们称 \(d_i=c_{i+1}\) 为一次匹配,贡献为 \(1\)
在不考虑拆分的情况下,我们可以得到一个简单的计算答案的方法,假设第一个元素拆成 \(x\) 和 \(a_1-x\),那么我们从前往后递推,每次都优先将当前元素匹配上一个元素。
因为无论如何匹配贡献都至多为 \(1\),假设匹配到了 \(t\) 处无法匹配了,那么从 \(t+1\) 处开始相当于是任意拆出 \(x\) 和 \(a_{t+1}-x\),也即限制更松,更优。
我们现在需要一个方法来快速检查 \(1\sim t\) 能否匹配,我们发现可以设 \(a_1=x+(a_1-x)\)
那么:
\(a_2=(a_1-x)+(a_2-a_1+x)\)
\(a_3=(a_2-a_1+x)+(a_3-a_2+a_1-x)\)
依次类推,\(1\sim t\) 能否匹配当且仅当对于所有 \(i\) 均有 \(\sum_{j=1}^i (-1)^{i-j}a_j+(-1)^i\times x\ge 1\),且 \(x\ge 1\)
这样我们可以得到关于 \(x\) 的不等式,且只有下界和上界的限制 \(L,R\) 。
能够匹配到 \(t\) 当且仅当加入 \(t\) 后仍有 \(L\le R\)。
当然,更方便的处理是时刻维护 \(c_i\) 的取值范围,这样 \(d_i=c_{i+1}\to\) 若 \(c_i\in [L,R],d_i\in [a_i-R,a_i-L]\)(记得和 \(1\) 取 \(\max\))
现在考虑允许拆分后如何计算答案。
仍然考虑不允许拆分时的贪心,我们发现可以即使拆分和匹配并存,我们可以认为我们至少会获得拆分的贡献,然后匹配作为答案的增加量,这样每次的增量都至多为 \(1\),我们仍然可以基于贪心的角度来考虑问题,所以我们到达位置 \(i\) 时必然希望最大化其贡献和,同时希望我们的限制尽可能松。
通过观察我们发现我们到达位置 \(i\) 时可以选择的值有且仅有两种:
- 连续一段前缀均被匹配,此时我们有一个关于 \(x\) 的限制方程,或者说 \(c_i\) 存在一个取值范围 \([L,R]\)。
- 某个位置执行了拆分,这个时候其权值为 \(x=\frac{a_i}{2}\),同时我们可以往后递推 \(x'=a_{i+1}-x...\),假设沿途均合法,那么到达 \(t\) 处时我们得到其一种可能的取值使得他可以和之前的位置发生一次匹配。
我们发现第一类贡献是可以直接用之前的贪心处理的。
我们发现第二类贡献可能会有很多个,但是都是固定的 \(c_i'\)。
于是我们有一个\(\mathcal O(n^2)\) 的暴力做法为:
- 从前往后贪心,每次答案的增量为 \(0/1/2\),我们保留能够使得当前答案最大的 \(c_i\) 的可能的取值,然后根据 \(a_i\) 是否为偶数,能否成功匹配的 \(c_i\),如果是偶数还要根据能否拆分进行分类讨论,暴力维护可能的 \(c_i\) 即可。
我们发现对于第一类贡献的维护是 naive 的,考虑第二类贡献,我们发现假设 \(\frac{a_i}{2}\) 是当前一种合法的 \(c_i\),我们必然会选择其。此时贡献加 \(2\),否则加 \(1\)
然后我们会 \(c_i\leftarrow a_i-c_i\)
对于第二类贡献,不难发现我们需要支持的操作为:
- 判定 \(\frac{a_i}{2}\) 是否存在。
- 删去大于等于 \(a_i\) 的元素,插入 \(\frac{a_i}{2}\)。
- 将集合内所有数变成 \(a_i-x\),或者清空集合。
删除的过程可以暴力删,因为我们发现每个元素只会被删一次。
我们发现我们会进行集体增加,集体取反这两种操作,那么维护一个 +/- 标记,和一个集体增加标记,然后就可以使用 set 来维护了。
于是这个题就做完了,复杂度 \(\mathcal O(n\log n)\)
具体实现存在较多细节,需要仔细考虑。
\(Code:\)
#include<bits/stdc++.h>
using namespace std ;
#define Next( i, x ) for( register int i = head[x]; i; i = e[i].next )
#define rep( i, s, t ) for( register int i = (s); i <= (t); ++ i )
#define drep( i, s, t ) for( register int i = (t); i >= (s); -- i )
#define re register
#define int long long
int gi() {
char cc = getchar() ; int cn = 0, flus = 1 ;
while( cc < '0' || cc > '9' ) { if( cc == '-' ) flus = - flus ; cc = getchar() ; }
while( cc >= '0' && cc <= '9' ) cn = cn * 10 + cc - '0', cc = getchar() ;
return cn * flus ;
}
const int N = 5e5 + 5 ;
const int inf = 1e9 ;
int n, a[N], k, b, L, R ;
set<int> S ;
set<int>::iterator it ;
bool find(int x) {
return (((L <= x) && (x <= R)) | (S.find(k * (x - b)) != S.end())) ;
}
int st[N], top ;
void Del(int x) {
top = 0 ;
if( S.empty() ) return ;
if( k == 1 ) {
it = S.lower_bound(x - b) ;
for(; it != S.end(); ++ it) st[++ top] = *it ;
}
else {
it = S.upper_bound(b - x) ;
if( it == S.begin() ) return ;
-- it ;
for(; it != S.begin(); -- it) st[++ top] = *it ;
if( it == S.begin() ) st[++ top] = *it ;
} while( top ) S.erase(st[top]), -- top ;
}
void solve() {
n = gi() ; int ans = 0 ;
L = inf, R = 1, k = 1, b = 0 ;
rep( i, 1, n ) a[i] = gi() ;
rep( i, 1, n ) {
if( a[i] % 2 == 0 ) {
int u = a[i] / 2 ; Del(a[i]) ;
if( find(u) ) S.clear(), ans += 2, S.insert(k * (u - b)), L = inf, R = 1 ;
else {
++ ans, S.insert(k * (u - b)) ;
L = a[i] - L, R = a[i] - R,
swap( L, R ), L = max( L, 1ll ) ;
if( L > R ) L = inf, R = 1 ;
}
}
else {
int flag = 1 ;
Del(a[i]), L = a[i] - L, R = a[i] - R,
swap( L, R ), L = max( L, 1ll ) ;
if( L > R ) L = inf, R = 1, flag = 0 ;
if(S.empty()) {
if( flag ) ++ ans ;
else L = 1, R = a[i] - 1 ;
}
else ++ ans ;
}
k = -k, b = -b, b += a[i] ;
}
cout << 2 * n - ans << endl ;
S.clear() ;
}
signed main()
{
int T = gi() ;
while( T-- ) solve() ;
return 0 ;
}