最小内向森林算法(HDU 6811 Joyful Party)

题目链接

题目大意

给定一个 n n n 个点的有向图,连边方式是从点 x x x 向区间 [ l , r ] [l,r] [l,r] 中的所有点连一条权值为 c c c 的有向边,求该图不小于 K K K 棵树的最大内向森林权值。

题解

首先把边权取反,转化成最小内向森林问题。

最小内向森林

注意到最小内向森林权值关于树的个数是个凸函数。于是二分斜率 k k k,建一个新点 T T T,所有点向 T T T 连一条权值为 k k k 的边,然后求最小树形图即可。最小树形图算法是可以使用可并堆(线段树合并)优化到 O ( E log ⁡ E ) O(E \log E) O(ElogE) 的,过程大致如下:

  1. 对于每个点找到其最小出边。用堆维护每个点的所有出边。
  2. 随便选择一个没有选择出边的点,选择其最小出边,并使答案加上最小出边。如果其最小出边通向的点和当前点在同一棵树中(即形成环),则进行步骤 3. 否则进行 5.
  3. 在当前点的出边堆上打标记,把所有出边的权值减掉最小出边的权值(把最小出边的权值调整成 0)。
  4. 遍历环上所有点,合并这些点对应的出边堆,也用并查集把这些点缩成一个点。继续步骤 5.
  5. 如果当前已经连了 n − 1 n-1 n1 条边,结束。否则返回 2.

每条边只会被删一次,所以复杂度为 O ( E log ⁡ E ) O(E \log E) O(ElogE)

加上凸优化,复杂度为 O ( E log ⁡ E log ⁡ ϵ ) O(E \log E \log \epsilon) O(ElogElogϵ),其中 ϵ \epsilon ϵ 为二分值域。

比赛的时候 djq 写了一发,实测常数太大无法通过。

优先内向树扩张算法

WC 营员交流时队爷 zzy 的做法,学习了一波。

考虑从 i + 1 i+1 i+1 棵内向树推出 i i i 棵内向树时的答案。我们对于当前每棵内向树定义“扩展代价”,表示其连出一条到其他树的边所需要的最小代价。考虑到最小内向森林问题是个拟阵,我们每次可以贪心扩展“扩展代价”最小的内向树。大致步骤如下:

  1. 用堆维护当前所有内向树及其扩展代价。初始内向树是 n n n 个点,代价是最小出边。
  2. 选择当前扩展代价最小的内向树,如果没有树可以扩展,则退出。否则进行扩展(即从根连其最小出边到其他树),记录答案。
  3. 更新当前被修改的内向树的“扩展代价”。返回 2.

但是如何更新当前被修改的内向树的代价呢?其实和 O ( E log ⁡ E ) O(E \log E) O(ElogE) 求最小树形图差不多。即每次选根的最小出边,如果成环则合并,否则就更新成功。注意时刻控制非根节点的最小出边权值为 0

事实上也不算很难,下面是用这种算法实现 loj140 最小树形图 的代码:

#include <bits/stdc++.h>
typedef long long LL;
using namespace std;
template<typename T> inline void chkmin(T &a, const T &b) { a = a < b ? a : b; }
template<typename T> inline void chkmax(T &a, const T &b) { a = a > b ? a : b; }

const int MAXN = 10005;
struct Node {
	int v, w, h, tag, ls, rs;
} nd[MAXN];
int n, m, rt, pq[MAXN], tpar[MAXN], spar[MAXN], nxt[MAXN], val[MAXN];

int find(int *par, int x) {
	return x == par[x] ? x : par[x] = find(par, par[x]);
}

inline void push_down(int k) {
	if (!nd[k].tag) return;
	int ls = nd[k].ls, rs = nd[k].rs, &t = nd[k].tag;
	nd[ls].w += t, nd[ls].tag += t;
	nd[rs].w += t, nd[rs].tag += t;
	t = 0;
}

int merge(int x, int y) {
	if (!x || !y) return x + y;
	push_down(x);
	push_down(y);
	if (nd[x].w > nd[y].w) swap(x, y);
	nd[x].rs = merge(nd[x].rs, y);
	if (nd[nd[x].ls].h < nd[nd[x].rs].h) swap(nd[x].ls, nd[x].rs);
	nd[x].h = nd[nd[x].rs].h + 1;
	return x;
}

inline void to_zero(int x) {
	nd[x].tag -= nd[x].w;
	nd[x].w = 0;
}

inline void pop(int &x) {
	push_down(x);
	x = merge(nd[x].ls, nd[x].rs);
}

struct Data {
	int u, w;
	bool operator==(const Data &d) const { return u == d.u && w == d.w; }
	bool operator<(const Data &d) const { return w == d.w ? u > d.u : w > d.w; }
};
struct Set {
	priority_queue<Data> del, ins;
	void push(const Data &d) { ins.push(d); }
	void erase(const Data &d) { del.push(d); }
	void upd() { while (!del.empty() && del.top() == ins.top()) del.pop(), ins.pop(); }
	bool empty() { upd(); return ins.empty(); }
	Data top() { upd(); return ins.top(); }
	void pop() { upd(); ins.pop(); }
} ss;

int main() {
	scanf("%d%d%d", &n, &m, &rt);
	for (int i = 1; i <= m; i++) {
		int u, v, w; scanf("%d%d%d", &u, &v, &w);
		if (u == v || v == rt) continue;
		nd[i] = Node { u, w, 1, 0, 0, 0 };
		pq[v] = merge(pq[v], i);
	}
	for (int i = 1; i <= n; i++) spar[i] = tpar[i] = i;
	for (int i = 1; i <= n; i++) if (i != rt) {
		if (!pq[i]) return puts("-1"), 0;
		ss.push(Data { i, val[i] = nd[pq[i]].w });
	}
	int ans = 0;
	for (int i = 1; i < n; i++) {
		if (ss.empty()) return puts("-1"), 0;
		int u = ss.top().u, w = ss.top().w;
		ans += w, ss.pop();
		to_zero(pq[u]);
		tpar[u] = find(tpar, nxt[u] = nd[pq[u]].v);
		pop(pq[u]);
		u = tpar[u];
		
		if (pq[u]) {
			ss.erase(Data { u, nd[pq[u]].w });
			w = 0;
			while (find(tpar, nd[pq[u]].v) == u) {
				int v = find(spar, nd[pq[u]].v);
				w += nd[pq[u]].w;
				to_zero(pq[u]);
				pop(pq[u]);
				while (v != u) {
					pq[u] = merge(pq[u], pq[v]);
					spar[v] = u;
					v = find(spar, nxt[v]);
				}
			}
			if (pq[u]) {
				nd[pq[u]].w += w, nd[pq[u]].tag += w;
				ss.push(Data { u, nd[pq[u]].w });
			}
		}
	}
	printf("%d\n", ans);
	return 0;
}

这种算法是严格强于最小树形图的,它可以求出恰好 x x x 棵内向树时,最小内向森林的代价。

HDU 6811

知道了上面的东西,这题就基本变成板子了。唯一不同的就是由于点向区间连边,暴力连的话会爆炸。可以线段树优化建图(?),但事实上不需要这样白白多个 log ⁡ \log log

我们可以在找最小出边的时候,在线段树上查询非自环的最小出边,这样仍然能够保证每条边被访问到时要么缩了两棵树、要么缩了两个点。所以复杂度仍然正确。

具体的,我们额外维护一棵线段树表示对于每个点,和它缩到一起的点有哪些。缩点时顺便线段树合并即可。

#include <bits/stdc++.h>
typedef long long LL;
using namespace std;
template<typename T> inline void chkmin(T &a, const T &b) { a = a < b ? a : b; }
template<typename T> inline void chkmax(T &a, const T &b) { a = a > b ? a : b; }

const int MAXN = 200005, MAXT = 2000005;
struct Node {
	int l, r; LL w; int h, tag, ls, rs;
} nd[MAXN];
int T, n, m, K, tot, rt[MAXN], pq[MAXN], tpar[MAXN], wt[MAXN];
int spar[MAXN], nxt[MAXN], ls[MAXT], rs[MAXT], cnt[MAXT];

int newnode() {
	int k = ++tot;
	cnt[k] = ls[k] = rs[k] = 0;
	return k;
}

void modify(int p, int &k, int l = 1, int r = n) {
	if (!k) k = newnode();
	++cnt[k];
	if (l == r) return;
	int mid = (l + r) >> 1;
	if (p <= mid) modify(p, ls[k], l, mid);
	else modify(p, rs[k], mid + 1, r);
}

int ask(int a, int &k, int l = 1, int r = n) {
	if (r < a) return n + 1;
	int mid = (l + r) >> 1;
	if (l >= a) {
		if (!k) return l;
		if (cnt[k] == r - l + 1) return n + 1;
		if (cnt[ls[k]] == mid - l + 1) return ask(a, rs[k], mid + 1, r);
		return ask(a, ls[k], l, mid);
	}
	int p = ask(a, ls[k], l, mid);
	if (p <= n) return p;
	return ask(a, rs[k], mid + 1, r);
}

int merge_seg(int x, int y, int l = 1, int r = n) {
	if (!x || !y) return x + y;
	int mid = (l + r) >> 1;
	if (l == r) { cnt[x] |= cnt[y]; return x; }
	ls[x] = merge_seg(ls[x], ls[y], l, mid);
	rs[x] = merge_seg(rs[x], rs[y], mid + 1, r);
	cnt[x] = cnt[ls[x]] + cnt[rs[x]];
	return x;
}

int ask_sum(int a, int b, int k, int l = 1, int r = n) {
	if (!k || a > r || b < l) return 0;
	if (a <= l && b >= r) return cnt[k];
	int mid = (l + r) >> 1;
	return ask_sum(a, b, ls[k], l, mid) + ask_sum(a, b, rs[k], mid + 1, r);
}

int find(int *par, int x) {
	return x == par[x] ? x : par[x] = find(par, par[x]);
}

inline void push_down(int k) {
	if (!nd[k].tag) return;
	int ls = nd[k].ls, rs = nd[k].rs, &t = nd[k].tag;
	nd[ls].w += t, nd[ls].tag += t;
	nd[rs].w += t, nd[rs].tag += t;
	t = 0;
}

int merge(int x, int y) {
	if (!x || !y) return x + y;
	push_down(x);
	push_down(y);
	if (nd[x].w > nd[y].w) swap(x, y);
	nd[x].rs = merge(nd[x].rs, y);
	if (nd[nd[x].ls].h < nd[nd[x].rs].h) swap(nd[x].ls, nd[x].rs);
	nd[x].h = nd[nd[x].rs].h + 1;
	return x;
}

inline void to_zero(int x) {
	nd[x].tag -= nd[x].w;
	nd[x].w = 0;
}

inline void pop(int &x) {
	push_down(x);
	x = merge(nd[x].ls, nd[x].rs);
}

struct Data {
	int u, v; LL w;
	bool operator==(const Data &d) const { return u == d.u && w == d.w && v == d.v; }
	bool operator<(const Data &d) const {
		return w == d.w ? (u == d.u ? v > d.v : u > d.u) : w > d.w;
	}
};
struct Set {
	priority_queue<Data> del, ins;
	void push(const Data &d) { ins.push(d); }
	void erase(const Data &d) { del.push(d); }
	void upd() { while (!del.empty() && del.top() == ins.top()) del.pop(), ins.pop(); }
	bool empty() { upd(); return ins.empty(); }
	Data top() { upd(); return ins.top(); }
	void pop() { upd(); ins.pop(); }
	void clear() { while (!del.empty()) del.pop(); while (!ins.empty()) ins.pop(); }
} ss;

int main() {
	freopen("input.txt", "r", stdin);
	for (scanf("%d", &T); T--;) {
		memset(pq, 0, sizeof(pq));
		memset(rt, 0, sizeof(rt));
		ss.clear();
		scanf("%d%d%d", &n, &K, &m);
		tot = 0;
		for (int i = 1; i <= m; i++) {
			int u, l, r, w; scanf("%d%d%d%d", &u, &l, &r, &w);
			if (l == r && l == u) continue;
			nd[i] = Node { l, r, -w, 1, 0, 0, 0 };
			pq[u] = merge(pq[u], i);
		}
		for (int i = 1; i <= n; i++) {
			spar[i] = tpar[i] = i;
			modify(i, rt[i]);
		}
		for (int i = 1; i <= n; i++) {
			if (!pq[i]) continue;
			int l = nd[pq[i]].l;
			ss.push(Data { i, wt[i] = l == i ? l + 1 : l, nd[pq[i]].w });
		}
		LL ans = 0, res = 0;
		for (int i = 1; n - i >= K; i++) {
			if (ss.empty()) break;
			int u = ss.top().u; LL w = ss.top().w;
			ans += w, ss.pop();
			to_zero(pq[u]);
			tpar[u] = find(tpar, nxt[u] = wt[u]);
			if (ask_sum(nd[pq[u]].l, nd[pq[u]].r, rt[u]) == nd[pq[u]].r - nd[pq[u]].l + 1) pop(pq[u]);
			u = tpar[u];
			
			if (pq[u]) {
				ss.erase(Data { u, wt[u], nd[pq[u]].w });
				w = 0;
				while (pq[u]) {
					int l = nd[pq[u]].l, r = nd[pq[u]].r, v;
					while ((v = ask(l, rt[u])) <= r && find(tpar, v) == u) {
						v = find(spar, v);
						w += nd[pq[u]].w;
						to_zero(pq[u]);
						while (v != u) {
							pq[u] = merge(pq[u], pq[v]);
							spar[v] = u;
							rt[u] = merge_seg(rt[u], rt[v]);
							v = find(spar, nxt[v]);
						}
					}
					if (v <= r) {
						nd[pq[u]].w += w, nd[pq[u]].tag += w;
						ss.push(Data { u, wt[u] = v, nd[pq[u]].w });
						break;
					}
					pop(pq[u]);
				}
			}
			chkmax(res, -ans);
		}
		printf("%lld\n", res);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值