Codeforces 1286F - Harry The Potter(折半搜索+DP)

Codeforces 题面传送门 & 洛谷题面传送门

一道代码 1k 题,然而我在传引用方面犯了一个智障错误导致我从昨天晚上调到今天早上……

首先考虑在二类操作对应的 \(i,j\) 之间连边,那么显然不会成环,否则可以调整全用 \(1\) 类操作不会更劣。证明很 easy。而显然假设 \(C\) 为我们连出来的森林的连通块个数,那么操作次数就是 \(n-C\),因此我们要最大化 \(C\)

我们先 check 一遍每个连通块是否可以用构成一棵树的 \(2\) 操作得到,记为 \(f_S\),那么后半部分是容易的,直接对 \(f_S\) 做一遍类似于子集卷积的东西即可,只不过这里是 \(\max\) + 卷积所以只能 \(3^n\) 做,好在此题时限较大直接这么做就能过(甚至加点剪枝可以跑到 <1s),因此我们着重讨论前面的部分,即 check 一个集合能否用一棵树操作得到。

注意到 \(2\) 操作是将 \(a_i\) 减去 \(x\)\(a_j\) 减去 \(x+1\),这个 \(+1\) 有点讨厌,我们不妨先将它去掉看看合法的充要条件是什么。我们先假设知道了树的形态,看看什么样的树的形态能够赋上合适的边权使得其能够恰好被清零,根据树是二分图的性质,我们先将树进行二分图染色,不难发现每次恰好对一个黑点和一个白点进行操作,因此一个必要条件是黑点点权和 \(B\) 等于白点点权和 \(W\),而这也是必要的,因为每次可以从叶子开始操作,根据 \(B=W\) 容易说明最后根节点点权为零。

接下来我们假设树的形态不固定考虑如何 check——注意我们黑点和白点是可以随便分配的——只要它们不为空即可,因此等价于能否将 \(S\) 分为两个非空集合 \(B,W\) 使得它们点权和相等。这个想怎么做就怎么做。

接下来再考虑加上 \(+1\) 的条件之后怎么处理,注意到每次 \(+1\) 会使得 \(B,W\) 之一点权和领先对方 \(1\),因此只有当 \(B,W\) 点权和差的绝对值 \(\le |S|-1\) 才有可能最终达到相等的状态,再其次,每次操作会使 \(B,W\) 点权和差的奇偶性改变,因此还要求 \(S\) 中点权和与 \(|S|-1\) 相同。

这样一来 check 的思路就变得清晰多了:判断 \(S\) 中点权和是否与 \(|S|-1\) 相同,以及 \(S\) 能否被划分成两个非空集合使得 \(B,W\) 点权和之差绝对值 \(\lt |S|\)。显然有简单的 \(2^{|S|}\),对所有 \(S\) 执行这样的 check 复杂度是 \(3^n\),有点慢了(虽然 DP 那部分也是 \(3^n\) 的,不过那部分剪剪枝常数可以变得很小,而这部分则不然),不知道过不过得去。考虑优化,注意到这题只涉及差,因此可以考虑折半搜,对一个集合所有子集点权和大小排序可以通过 DFS + 归并排序做到 \(2^{\frac{|S|}{2}}\),然后跑个 two pointers,这部分复杂度 \(\sum\limits_{S}\sqrt{2}^{|S|}=(1+\sqrt{2})^n\)

这样总复杂度就是一个常数巨小的 \(3^n\),可以通过此题并且跑了时限的 \(\dfrac{1}{5}\)

const int MAXN = 20;
const int MAXP = 1 << 20;
int n, lg[MAXP + 5], dp[MAXP + 5]; ll a[MAXN + 5], sum[MAXP + 5];
int bt[MAXN + 5], len = 0;
vector<ll> getseq(int l, int r) {
	if (l > r) return vector<ll>{0};
	vector<ll> v1 = getseq(l + 1, r), v2 = v1, v;
	int p1 = 0, p2 = 0, len = (1 << r - l);
	for (int i = 0; i < len; i++) v2[i] = v1[i] + a[bt[l]], v1[i] -= a[bt[l]];
	while (p1 < len || p2 < len) {
		if (p2 >= len || (p1 < len && v1[p1] < v2[p2])) v.push_back(v1[p1++]);
		else v.push_back(v2[p2++]);
	}
	return v;
}
bool check(int S) {
	if (__builtin_popcount(S) == 1 && !sum[S]) return 1;
	len = 0; for (int i = 1; i <= n; i++) if (S >> (i - 1) & 1) bt[++len] = i;
	if (sum[S] - len + 1 & 1) return 0; int mid = len >> 1;
	vector<ll> seq1 = getseq(1, mid);
	vector<ll> seq2 = getseq(mid + 1, len);
	int l1 = 1 << mid, l2 = 1 << len - mid; seq2.push_back(1e18);
	int nd = 1 + (abs(sum[S]) < len) * 2;
	for (int i = 0, L = l2, R = l2; i < l1; i++) {
		while (~R && seq1[i] + seq2[R] >= len) --R;
		while (~L && seq1[i] + seq2[L] > -len) --L; ++L;
		nd -= min(nd, R - L + 1);
	}
	return (nd == 0);
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%lld", &a[i]), lg[1 << i] = i;
	for (int i = 1; i < (1 << n); i++) sum[i] = sum[i & (i - 1)] + a[lg[i & (-i)] + 1];
	for (int i = 1; i < (1 << n); i++) if (!dp[i] && check(i)) {
		int rst = ((1 << n) - 1) ^ i;
		for (int j = rst; ; --j &= rst) {
			chkmax(dp[i | j], dp[j] + 1);
			if (!j) break;
		}
	}
	printf("%d\n", n - dp[(1 << n) - 1]);
	return 0;
}
posted @ 2022-07-09 08:36  tzc_wk  阅读(41)  评论(0编辑  收藏  举报