pjudge 21614 【PR #1】守卫

首先尝试去转化每个村子恰好可以被一个守卫到达这个限制。
因为目标是让某一个村子与有守卫的村子连通,所以可以直接当作所有守卫所在的村子连通,那么最小生成树就是答案。

考虑此时的最小生成树除掉钦定守卫间连通的边满足什么条件。
因为实质上也是要保证连通,所以这些原树的的边一定存在于原树的最小生成树中,因为如果不存在就可以用最小生成树中的边去替换掉这条边而依然保证连通。
于是接下来就把一张图缩小成了树来考虑。

考虑知道了哪些点有守卫如何求解答案。
直接跑最小生成树不是很好求解,于是考虑反过来做,考虑哪些最小生成树中的边是无用的,可以删掉。
首先如果只有两个点 \(u, v\) 有守卫,那么这就说明 \((u, v)\) 的这个路径中是可以删掉一条边的,那么贪心的,一定会选择最大的一条边删掉。

接下来考虑刻画这个贪心的选择最大边权的边的条件。
那么可以考虑建出 kruskal 重构树,那么非叶子的点就代表一条边,令其点权就为实际树上的边权。
这样做的一个好处是 \(\operatorname{lca}(u, v)\) 就是路径上的最大边权。

接下来考虑如果有三个点 \(u, v, w\) 有守卫这个形态又会成什么样子:
首先 \(\operatorname{lca}(u, v, w)\) 一定是可以被删掉的,因为此时两个子树一定都有守卫且这个边权最大。
接下来就会删掉 \(\operatorname{lca}(u, v, w)\),并分成两个子树,不妨假设 \(u, v\) 在一个子树,那么依然会像只有两个守卫一样贪心的选择 \(\operatorname{lca}(u, v)\)

继续扩展,能够知道的是,删掉的点一定是把有守卫的点拿出来建虚树,这颗虚树上的非叶子节点。
这是因为对于虚树上的非叶子节点都满足其左儿子和右儿子都有守卫,所以这个点是可以被删的,其次它也是子树中点权最大的点,删掉这个点一定不劣。

于是接下来考虑刻画删掉虚树上的非叶子节点这个限制。
因为对于 \(k\) 个守卫都可以选择不同的点且在 \(w_i = 1\) 的情况下就需要二分图匹配,这启发去考虑网络流建模。
且在 \(w_i = 1\) 的情况下就是正常的权值为 \(1\) 的二分图匹配,而要解决的问题还需要考虑点权不相同,于是可以考虑用费用流来刻画。

首先对于守卫的连边就简单了,因为每个守卫只能选一个点,就可以当作是流过的流量只能为 \(1\)
于是可以连边 \((S, p_i, 1, 0)\) 代表第 \(i\) 个守卫只能选一个点,连边 \((p_i, s_{i, j}, 1, 0)\) 当作为每个守卫选择一个点。

接下来因为最后要删掉的点是与虚树相关的,所以应该要把虚树用流刻画出来。
对此,可以考虑连边 \((i, \operatorname{fa}_i, 1, 0)\),代表 \(i\) 号点的流向上流,这样子就可以把这个虚树的形态提出来。

接下来还要考虑如何刻画被删掉的点。
此时回到上文的“对于虚树上的非叶子节点都满足其左儿子和右儿子都有守卫”,那么这就说明点 \(i\) 的左右儿子都会传来一个大小为 \(1\) 的流并在此相交,那么就可以在这个时候把其中一条流拎出来当作被删掉的贡献(这也就是上文 \(i\) 只能往 \(\operatorname{fa}_i\)\(1\) 的原因,因为需要让 \(1\) 个流量在此终止),连边 \((i, T, 1, -v_i)\)
这个连边的正确性也是基于贪心的,因为越往上 \(v_i\) 越大,\(- v_i\) 就越小,所以每一条流肯定会尽量往上流直到碰到一条流被终止,那么这个点就是可以被删掉的。

此时还有一个小问题,就是对于虚树的这个根依然还有一条流往上流。
但是此时恰好还要判断每个森林是否会被流过(有守卫),于是考虑利用这个流。
考虑因为贪心和费用流都基于 kruskal 重构树的往上走点权变大这一性质,考虑继续沿用这个性质,对于每颗树,考虑给根节点一个父亲 \(rt\),并把权值当作 \(+\infty\),即连边 \((rt, T, 1, -\infty)\)
这样就可以让那条多出的流走到这个 \(rt\) 并走掉,这样子顺带也可以判断每个连通块是否被流过,因为只要被流过就一定会流过这个 \(-\infty\) 的边。

接下来还要考虑一个无解的情况就是多个守卫选到一个点的情况。
但是能够知道其实前面的建模已经处理了这种情况:对于每个非叶子节点只会往父亲流 \(1\)
于是只需要判断最后的最大流是否为 \(k\) 就可以了。

于是可以直接跑最小费用最大流了,用原始对偶实现可以做到 \(\mathcal{O}(n(n + k)^2)\)

碎碎念:vp 写挂原始对偶挂了 25,还有 dinic 怎么跑这么快。

#include<bits/stdc++.h>
using ll = long long;
constexpr ll inf = 1e9;
namespace flow {
   int N, S, T;
   constexpr int maxn = 900 + 10, maxm = 91500 + 100;
   int fir[maxn], to[maxm * 2], fl[maxm * 2], co[maxm * 2], nxt[maxm * 2], tot = 1;
   inline void add(int x, int y, int f, int c, bool fi = true) {
      tot++, to[tot] = y, fl[tot] = f, co[tot] = c, nxt[tot] = fir[x], fir[x] = tot;
      if (fi) add(y, x, 0, -c, false);
   }
   ll d[maxn], dis[maxn]; int lst[maxn]; bool vis[maxn];
   inline void SPFA() {
      std::fill(d + 1, d + N + 1, inf);
      std::queue<int> Q;
      d[S] = 0, Q.push(S);
      while (! Q.empty()) {
         int u = Q.front(); Q.pop();
         vis[u] = false;
         for (int i = fir[u]; i; i = nxt[i]) {
            int v = to[i], w = co[i];
            if (fl[i] && d[u] + w < d[v]) {
               d[v] = d[u] + w;
               if (! vis[v]) {
                  vis[v] = true, Q.push(v);
               }
            }
         }
      }
   }
   std::pair<int, ll> mcmf() {
      SPFA();
      int F = 0; ll C = 0;
      for (; ; ) {
         std::fill(dis, dis + N + 1, inf);
         memset(vis + 1, 0, sizeof(bool) * N);
         dis[S] = 0;
         for (; ; ) {
            int u = 0;
            for (int i = 1; i <= N; i++) {
               if (! vis[i] && dis[i] < dis[u]) {
                  u = i;
               }
            }
            if (! u) break;
            vis[u] = true;
            for (int i = fir[u]; i; i = nxt[i]) {
               int v = to[i]; ll  w = d[u] + co[i] - d[v];
               if (fl[i] && dis[u] + w < dis[v]) {
                  dis[v] = dis[u] + w, lst[v] = i;
               }
            }
         }
         if (dis[T] == inf) break;
         F++, C += dis[T] + d[T];
         for (int u = T; u != S; ) {
            fl[lst[u]]--, fl[lst[u] ^ 1]++;
            u = to[lst[u] ^ 1];
         }
         for (int i = 1; i <= N; i++) {
            d[i] += dis[i];
         }
      }
      return {F, C};
   }
}
constexpr int maxn = 3e2 + 10;
int n, m, k;
struct edge {
   int u, v, w;
} e[maxn * maxn / 2];
int fa[maxn * 2], val[maxn * 2], ft[maxn * 2];
inline int getfa(int x) {
   while (fa[x] != x) x = fa[x] = fa[fa[x]];
   return x;
}
int main() {
   scanf("%d%d%d", &n, &m, &k);
   for (int i = 1; i <= m; i++) {
      int u, v, w;
      scanf("%d%d%d", &u ,&v, &w);
      e[i] = {u, v, w};
   }
   std::sort(e + 1, e + m + 1,
             [&](const edge &x, const edge &y) {
                return x.w < y.w;
             });
   std::iota(fa + 1, fa + n * 2, 1);
   ll sum = 0; int  N = n;
   for (int i = 1; i <= m; i++) {
      int x = getfa(e[i].u), y = getfa(e[i].v);
      if (x == y) continue;
      val[++N] = e[i].w, ft[x] = ft[y] = fa[x] = fa[y] = N;
      sum += e[i].w;
   }
   for (int i = 1, N_ = N; i <= N; i++) {
      if (! ft[i]) {
         val[++N_] = 1001, ft[i] = N_;
         sum += 1001;
      }
   }
   flow::S = n * 2 + k + 1, flow::T = flow::N = n * 2 + k + 2;
   for (int i = n + 1; i <= n * 2; i++) flow::add(i, flow::T, 1, -val[i]);
   for (int i = 1; i <= N; i++) flow::add(i, ft[i], 1, 0);
   for (int i = 1, s; i <= k; i++) {
      flow::add(flow::S, n * 2 + i, 1, 0);
      scanf("%d", &s);
      for (int j; s--; ) {
         scanf("%d", &j), flow::add(n * 2 + i, j, 1, 0);
      }
   }
   auto [F, C] = flow::mcmf();
   if (F != k) return puts("-1"), 0;
   for (int i = flow::fir[flow::T]; i; i = flow::nxt[i]) {
      if (flow::co[i] == 1001 && flow::fl[i] == 0) {
         return puts("-1"), 0;
      }
   }
   printf("%lld\n", sum + C);
   return 0;
}
posted @ 2025-02-20 19:39  rizynvu  阅读(26)  评论(0)    收藏  举报