CF1408H Rainbow Triples
Solution
题意已经很清楚了。
考虑这样一个三元组的位置,首先是 \(b\) 要选在非 \(0\) 位,\(a,c\) 分别要在它的左右两边。那么有一个很明显的答案上界就是 \(\dfrac{n_0}{2}\),其中 \(n_0\) 表示序列中 \(0\) 的个数。
然后来考虑这个问题第一困难的地方,是你需要考虑每个元素同时两边的 \(0\) 的个数,这使得我们非常头痛,于是我们依据 \(\dfrac{n_0}{2}\) 把序列分成两部分 \(L,R\)。这样 \(L\) 就只需要考虑它左边的 \(0\) 是否足够,\(R\) 只需要考虑它右边的 \(0\) 是否足够,另一边不需要考虑。因为如果另一边也不够那么考虑的这一边必然是不够的。
第二困难的地方是想到网络流建模。首先考虑每个 \(0\) 只能被用 \(1\) 次,所以我们从每个 \(0\) 向终点连一条容量为 \(1\) 的边。注意到题目中还有颜色的限制,于是我们从起点向各个颜色连一条容量为 \(1\) 的边从而保证了每种颜色都不重复。然后颜色向对应节点连边……不,不需要,我们发现这条边只需要给全村的希望就可以了,也就是最有可能被选出的点,即左边最右的点和右边最左的点。然后各个节点向其左右的 \(0\) 分别连边……哦会爆炸,于是我们在左边后一个向前一个连一条边,右边后一个向前一个连一条边,容量正无穷,替代了一个一个连边。建完大概是长这样的:
然后最大流就是答案。
显然跑不过去,于是 new trick:模拟网络流。一般考虑最大流=最小割,然后枚举最小割。由于别的边都是正无穷的,而删中间的 \(1\) 边必定劣于割源点到颜色的边,所以我们只可能割颜色到源点的边和 \(0\) 到汇点的边。如果一个颜色被选中不割了,那么这个颜色所连到的所有 \(0\) 都要割掉,即其前缀和一些后缀。于是我们枚举割掉的前缀的 \(0\) 的数量,然后后缀 \(0\) 的数量用线段树维护,那要割的颜色边数量就确定了。
实现方法是采用线段树,下标为 \(i\) 的节点存储后缀割去 \(i\) 条 \(0\) 边所要割掉的颜色边加上后缀 \(0\) 边数量。
- 枚举 \(L\) 中的一个非 \(0\) 元素,它前面的 \(0\) 都割掉,那么当 \(R\) 中割去的 \(0\) 边也大于其后缀时,当前元素都不需要断掉颜色边,所以线段树上区间减一,表示这条边不需要断掉了。
- 在此之前,如果一种颜色没有出现在 \(L\) 中,那么在 \(R\) 中割掉一定的后缀数量也会使得这条颜色边不需要割。
- 枚举的时候,如果一种元素没有出现在 \(R\) 中,那么在 \(R\) 中全局减一,因为这个颜色无论后面怎么割都不需要割了。
然后每次取最小值就是答案了。
Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
using namespace std;
const int MAXN=5e5+10;
struct Tree{int l,r,mn,inc;}tr[MAXN<<2];
#define ls i<<1
#define rs i<<1|1
int Col;
void pushup(int i){tr[i].mn=min(tr[ls].mn,tr[rs].mn);}
void build(int i,int l,int r){
tr[i].l=l;tr[i].r=r;tr[i].inc=0;
if(l==r){tr[i].mn=Col+l;return;}
int mid=l+r>>1;build(ls,l,mid);build(rs,mid+1,r);
pushup(i);
}
void add(int i,int v){tr[i].mn+=v;tr[i].inc+=v;}
void pushdown(int i){
if(!tr[i].inc) return;
add(ls,tr[i].inc);add(rs,tr[i].inc);
tr[i].inc=0;
}
void upd(int i,int l,int r,int v){
if(tr[i].l==l&&tr[i].r==r){add(i,v);return;}
pushdown(i);int mid=tr[i].l+tr[i].r>>1;
if(r<=mid) upd(ls,l,r,v);else if(l>mid) upd(rs,l,r,v);
else upd(ls,l,mid,v),upd(rs,mid+1,r,v);
pushup(i);
}
int ask(int i,int x){
if(tr[i].l==tr[i].r) return tr[i].mn;
pushdown(i);int mid=tr[i].l+tr[i].r>>1;
if(x<=mid) return ask(ls,x);else return ask(rs,x);
}
int p[MAXN],pre[MAXN],Cl[MAXN],Cr[MAXN];
vector<int> L,R;
void init(int n){
L.clear();R.clear();Col=0;
for(int i=1;i<=n;i++) Cl[i]=Cr[i]=0;
}
void solve(){
int n;scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&p[i]),pre[i]=pre[i-1]+(p[i]==0);
int n0=pre[n]/2;
for(int i=1;i<=n;i++){
if(!p[i]) continue;
if(pre[i]<=n0) L.pb(i);else R.pb(i);
}
for(int i=0;i<(int)L.size();i++) Cl[p[L[i]]]=L[i];
for(int i=(int)R.size()-1;i>=0;i--) Cr[p[R[i]]]=R[i];
for(int i=1;i<=n;i++) Col+=(Cl[i]||Cr[i]);
int tot=pre[n]-n0;build(1,0,tot);
// cerr<<"Seg Ele: ";for(int i=0;i<=tot;i++) cerr<<ask(1,i)<<' ';cerr<<'\n';
for(int i=1;i<=n;i++) if(!Cl[i]&&Cr[i]) upd(1,pre[n]-pre[Cr[i]-1],tot,-1);
// cerr<<"Seg Ele: ";for(int i=0;i<=tot;i++) cerr<<ask(1,i)<<' ';cerr<<'\n';
int ans=min(tr[1].mn,n0);
for(int i=1;i<=n&&pre[i]<=n0;i++){
if(Cl[p[i]]^i) continue;
if(Cr[p[i]]) upd(1,pre[n]-pre[Cr[p[i]]-1],tot,-1);
else upd(1,0,tot,-1);
// cerr<<"After "<<i<<" 0s\n";
// cerr<<"Seg Ele: ";for(int j=0;j<=tot;j++) cerr<<ask(1,j)<<' ';cerr<<'\n';
ans=min(ans,tr[1].mn+pre[i]);
}
printf("%d\n",ans);
init(n);
}
int main()
{
int T;
for(scanf("%d",&T);T--;)
solve();
return 0;
}