题目链接
给定一个 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) 的,过程大致如下:
每条边只会被删一次,所以复杂度为 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 棵内向树时的答案。我们对于当前每棵内向树定义“扩展代价”,表示其连出一条到其他树的边所需要的最小代价。考虑到最小内向森林问题是个拟阵,我们每次可以贪心扩展“扩展代价”最小的内向树。大致步骤如下:
但是如何更新当前被修改的内向树的代价呢?其实和 O ( E log E ) O(E \log E) O(ElogE) 求最小树形图差不多。即每次选根的最小出边,如果成环则合并,否则就更新成功。注意时刻控制非根节点的最小出边权值为 0。
事实上也不算很难,下面是用这种算法实现 loj140 最小树形图 的代码:
#include
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 棵内向树时,最小内向森林的代价。
知道了上面的东西,这题就基本变成板子了。唯一不同的就是由于点向区间连边,暴力连的话会爆炸。可以线段树优化建图(?),但事实上不需要这样白白多个 log \log log。
我们可以在找最小出边的时候,在线段树上查询非自环的最小出边,这样仍然能够保证每条边被访问到时要么缩了两棵树、要么缩了两个点。所以复杂度仍然正确。
具体的,我们额外维护一棵线段树表示对于每个点,和它缩到一起的点有哪些。缩点时顺便线段树合并即可。
#include
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;
}