← Home
/* 省选:2026.3.7 */
/* YMao99! */
/* Hatsune Miku x Kasane Teto */
#include <bits/stdc++.h>
#define lowbit(x) (x & (-x))
using namespace std;
 
const int N = 500010;
int T, n, cnt, a[N]; vector < int > tmp;
struct Range {int l, r, val;} s[N], t[N];
 
vector < Range > save[N];
int rt, idx;
struct SGT
{
	int ls, rs, maxn, tag;
	#define ls(x) tree[x].ls
	#define rs(x) tree[x].rs
	#define maxn(x) tree[x].maxn
	#define tag(x) tree[x].tag
} tree[N * 2];
inline void pushup(int now) {maxn(now) = max(maxn(ls(now)), maxn(rs(now)));}
inline void push_tag(int now, int _tag) {maxn(now) += _tag, tag(now) += _tag;}
inline void pushdown(int now)
{
	if(tag(now))
	{
		push_tag(ls(now), tag(now));
		push_tag(rs(now), tag(now));
		tag(now) = 0;
	}
}
inline void build(int &now, int l, int r)
{
	now = ++idx; maxn(now) = tag(now) = ls(now) = rs(now) = 0;
	if(l == r) return ;
	int mid = (l + r) >> 1;
	build(ls(now), l, mid), build(rs(now), mid + 1, r);
	pushup(now);
}
inline void range_add(int now, int l, int r, int L, int R, int num)
{
	if(L <= l && r <= R) {push_tag(now, num); return ;}
	pushdown(now); int mid = (l + r) >> 1;
	if(L <= mid) range_add(ls(now), l, mid, L, R, num);
	if(mid < R) range_add(rs(now), mid + 1, r, L, R, num);
	pushup(now);
}
inline void debug(int now, int l, int r)
{
	if(l == r)
	{
		cerr << maxn(now) << " ";
		return ;
	}
	pushdown(now); int mid = (l + r) >> 1;
	debug(ls(now), l, mid), debug(rs(now), mid + 1, r);
}
 
inline void sol()
{
	cin >> n; tmp.clear(); // tmp 存储了 0 的位置 
	set < int > S;
	for(int i = 1; i <= n; ++i) cin >> a[i], S.insert(a[i]);
	cnt = S.size() - S.count(0);
	for(int i = 1; i <= n; ++i) if(!a[i]) tmp.push_back(i);
	
	function < bool (int) > check = [&](int m)
	{
		// 检查前 m 个 0 和后 m 个 0 是否可以圈出 m 个数字,检查方式用模拟最小割 
//		cerr << "check " << m << " +++++++++++++++++++++++++++++++\n";
		
		for(int i = 1; i <= m; ++i) s[i] = {tmp[i - 1], tmp[tmp.size() - m - 1 + i]};
		Range global = {s[m].l, s[1].r}; int all = cnt; // global 是一定会被覆盖的区域,all 是元素个数 
		
		// 解析出扫描线 
		function < void (int, int, int, int) > get_rect = [&](int sx, int tx, int sy, int ty)
		{
//			cerr << "get_rect " << sx << " " << tx << " " << sy << " " << ty << '\n';
			save[sx].push_back({sy, ty, 1});
			if(tx + 1 > global.l) return ;
			save[tx + 1].push_back({sy, ty, -1});
		};
		for(int i = 1; i <= n + 1; ++i) save[i].clear(), t[i] = {-N, N};
		// 下端的贡献 
		for(int i = 1; i <= m; ++i) get_rect(1, s[i].l, s[i].r, n), ++all;
		// 上端的贡献 
		for(int i = 1; i < global.l; ++i) if(a[i]) t[a[i]].l = i;
		for(int i = n; i > global.r; --i) if(a[i]) t[a[i]].r = i;
		for(int i = global.l; i <= global.r; ++i) if(a[i]) t[a[i]] = {-N, N};
		for(int i = 1; i <= n; ++i)
		{
			if(t[i].l != -N || t[i].r != N)
			{
				int L = max(1, t[i].l + 1), R = min(n, t[i].r - 1);
				get_rect(L, R, L, R);
			}
		}
		
		// 求解最大值 
		rt = idx = 0; build(rt, global.r, n);
		int ans = all - m;
		for(int i = 1; i <= global.l; ++i)
		{
			for(Range rng : save[i]) range_add(rt, global.r, n, max(global.r, rng.l), rng.r, rng.val);
			ans = max(ans, maxn(rt));
			if(all - ans < m) return false; // 这个值就是最小割,即最大流 
//			cerr << i << " " << maxn(rt) << '\n';
//			cerr << "debug : "; debug(rt, 1, n); cerr << '\n';
		}
		
//		cerr << ans << '\n';
		
		return true;
	};
	
	int l = 1, r = min((int)tmp.size() / 2, cnt), ans = 0;
	while(l <= r)
	{
		int mid = (l + r) >> 1;
		if(check(mid)) ans = mid, l = mid + 1;
		else r = mid - 1;
	}
	cout << ans << '\n';
}
 
int main()
{
	#ifdef LOCAL
//	freopen("text.in", "r", stdin);
//	freopen("prog.out", "w", stdout);
	#endif
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	cin >> T;
	while(T--) sol();
	
	return 0;
}
/*
1
6
0 2 0 0 5 0
 
首先我们要使用的 0 肯定是一段前缀和一段后缀,并且是顺次匹配的 
因为这个 ( + X + ) 模型的左右括号大概满足一个四边形不等式,全都不包含的匹配自由度最高 
那么不妨考虑一下非零的数全都不同怎么做!可以二分答案 
其实不是全部相同也能二分答案,但是 check 就不显然了 
这是一个最大流问题,能不能用数据结构模拟呢
补怼,我有种转最小割的冲动,试试看能不能线性规划对偶一下
我去,可以,不过还是需要人为地求解最小割 
由于一侧的线段全是包含的,所以割掉的是一个前缀和一个后缀 
然后每种颜色正好又能转化为一个限制 (x, y) 表示保留的 [L, R] 满足 x <= L <= R <= y 时不用割 
这就是一个线段树直接做啊 做完了 
*/