2025-11-7 10:20:00 TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.

图论随笔

随笔

图论

最短路

Floyd

P8186 [USACO22FEB] Redistributing Gifts S

\(\color{#39C5BB}{[EASY]}\)

把每个奶牛看成节点,向可能索要礼物的奶牛连边,可以发现当且仅当某两个奶牛在一个环内可以换礼物。

跑传递背包,用bitset优化,时间复杂度为 \(\mathcal{O}(\frac{n^3}{w})\)

充分展示我有多菜

code
const int N = 505;

bitset<N> f[N];
int n, a[N][N];

int main() {
  read(n);
  rep(i, 1, n) rep(j, 1, n) read(a[i][j]);
  

  rep(i, 1, n) rep(j, 1, n) {
    if (a[i][j - 1] == i) break;
    f[i][a[i][j]] = 1;
  }

  rep(k, 1, n) rep(i, 1, n) if (f[i][k]) f[i] |= f[k];
  
  rep(i, 1, n) rep(j, 1, n) if (f[i][a[i][j]] && f[a[i][j]][i]) {
    write(a[i][j], '\n'); break;
  }
  return 0;
}

线段树优化建图

处理向区间连边时有效的算法,线段树上的点相当于虚拟节点,象征一段区间所有点。让虚拟节点访问真实的点是简单的,把线段树上节点与左右儿子的连接当成有向边,叶子节点建成真节点即可。

根据线段树上的方向可以把线段树分为两类:

  1. 出树:从节点到儿子,真实节点连边是从真实节点连向虚拟节点,象征真实节点向一段区间的所有真实节点连有向边

  2. 入树:与出树完全相反

// 建出树模板
void build(int &rt, int l, int r) {
  if (l == r) { return rt = l, void(); }
  rt = ++node;
  int mid = (l + r) >> 1;
  build(ls[rt], l, mid), build(rs[rt], mid + 1, r);
  add(rt, ls[rt], 0), add(rt, rs[rt], 0);
}

void update(int rt, int l, int r, int L, int R, int v, int w) {
  if (L <= l && r <= R) { return add(v, rt, w), void(); }
  int mid = (l + r) >> 1;
  if (L <= mid) update(ls[rt], l, mid, L, R, v, w);
  if (mid < R) update(rs[rt], mid + 1, r, L, R, v, w); 
}

CF786B Legacy

\(\color{#FFA500}[NORMAL-]\)

模板题。

操作二:从 \(v\) 向 “出树” 中象征 \([l, r]\) 区间的节点连边。

操作三:从“入树”中象征 \([l, r]\) 区间的节点向 \(v\) 连边。

然后跑 Dijkstra 就好了。

代码数组的大小应该有问题,一个节点最多连接 \(\log n\) 条边,但是没有这么强的数据。

code

P7984 [USACO21DEC] Tickets P

\(\color{#FFA500}{[NORMAL+]}\)

容易想到建反边,求出起点为 \(1\) 和起点为 \(n\) 的最短路树,用线段树优化是好做的。

但是结果并不能简单相加,因为可能 \(u \rightarrow 1\)\(u \rightarrow n\) 可以购买同一张票,票价会被算两次。

有个巧妙的想法,把票也建成点,经过该点说明买了票。而如果存在 \(dis_v > dis_u + w\),说明票被算了两次,直接更新掉。这形如松弛操作,送 Dijkstra 里跑就好。

code
const int N = 1e5 + 5;

int head[N * 5], edge[N * 70], Next[N * 70], ver[N * 70], tot = 1;
int n, k;
int R, node, ls[N * 4], rs[N * 4];
i64 _1[N * 5], _n[N * 5], d[N * 5];
priority_queue<pii, vector<pii>, greater<pii>> que;
bool vis[N * 5];

void add(int u, int v, int w) {
  Next[++tot] = head[u];
  ver[head[u] = tot] = v;
  edge[tot] = w;
}

void build(int &rt, int l, int r) {
  if (l == r) { return rt = l, void(); }
  rt = ++node;
  int mid = (l + r) >> 1;
  build(ls[rt], l, mid), build(rs[rt], mid + 1, r);
  add(ls[rt], rt, 0), add(rs[rt], rt, 0);
}

void update(int rt, int l, int r, int L, int R, int v, int w) {
  if (L <= l && r <= R) { return add(rt, v, w), void(); }
  int mid = (l + r) >> 1;
  if (L <= mid) update(ls[rt], l, mid, L, R, v, w);
  if (mid < R) update(rs[rt], mid + 1, r, L, R, v, w); 
}

void dijkstra(int st, i64 *dis, int k) {
  if (!k) rep(i, 1, node) dis[i] = 0x3f3f3f3f3f3f3f3f;
  memset(vis, 0, sizeof(vis));
  if (!k) dis[st] = 0, que.push({dis[st], st});
  else rep(i, 1, node) que.push({dis[i], i});

  while (que.size()) {
    int u = que.top().second; que.pop();
    if (vis[u]) continue;
    vis[u] = 1;
      
    for (int i = head[u]; i; i = Next[i]) {
      int v = ver[i], w = edge[i];
      
      if (dis[u] + w < dis[v]) {
        dis[v] = dis[u] + w;
        que.push({dis[v], v});
      }
    }
  }
}

int main() {
  read(n, k);  node = n;
  build(R, 1, n);

  rep(i, 1, k) {
    int u, w, l, r; read(u, w, l, r);
    int v = ++node; add(v, u, w);
    update(R, 1, n, l, r, v, 0);
  }
  
  dijkstra(1, _1, 0);
  dijkstra(n, _n, 0);
  rep(i, 1, node) d[i] = _1[i] + _n[i];
  
  dijkstra(1, d, 1);
  rep(i, 1, n) write(d[i] >= 0x3f3f3f3f3f3f3f3f ? -1 : d[i], '\n');
  return 0;
}

Tarjan 算法与有向图连通性

强连通分量

判定法则

\(x\) 回溯前,\(low(u) = dfn(u)\),则栈中从 \(x\) 到栈顶所有节点构成强连通分量。

void tarjan(int u) {
  dfn[u] = low[u] = ++dn, ins[u] = 1;
  st[++top] = u;
  
  adj(G, u, v) {
    if (!dfn[v]) tarjan(v), chkmin(low[u], low[v]);
    else if (ins[v]) chkmin(low[u], dfn[v]);
  }
  
  if (low[u] == dfn[u]) { // 缩点
   	col[u] = ++cn, ins[u] = 0;
    while (st[top] != u) col[st[top]] = cn, ins[st[top--]] = 0;
    top--;
  }
}

P9431 [NAPC-#1] Stage3 - Jump Refreshers

\(\color{#39C5BB}{[EASY]}\)

可以 \(\mathcal{O}(n^2)\) 建边,然后缩点跑最长路。

虽然简单,但是写红温了……

多熟悉模板吧

code
const int N = 3e3 + 5;
const int inf = 1e9;

int head[N], ver[N * N], Next[N * N], tot = 1;
int T, id, n, c, d, x[N], y[N];
int dfn[N], low[N], dn, st[N], top;
bool ins[N];
int col[N], cn;
int val[N], f[N], ans;
vector<int> g[N];

void add(int u, int v) {
  Next[++tot] = head[u];
  ver[head[u] = tot] = v;
}

void tarjan(int u) {
  dfn[u] = low[u] = ++dn; st[++top] = u, ins[u] = 1;
  for (int i = head[u]; i; i = Next[i]) {
    int v = ver[i];
    if (!dfn[v]) tarjan(v), chkmin(low[u], low[v]);
    else if (ins[v]) chkmin(low[u], dfn[v]);
  }

  if (dfn[u] == low[u]) {
    col[u] = ++cn, ins[u] = 0;
    while (st[top] != u) ins[st[top]] = 0, col[st[top--]] = cn;
    top--;
  }
}

void solve() {
  read(n, d, c);
  tot = 1, cn = 0, dn = 0;
  rep(i, 1, n) read(x[i], y[i]);
  rep(i, 1, n) rep(j, 1, n) {
    if (i == j) continue;
    if (x[j] >= x[i] && x[i] + y[i] + d >= x[j] + y[j]) add(i, j);
    if (x[j] < x[i] && y[i] - x[i] + d >= y[j] - x[j]) add(i, j);
  }

  rep(i, 1, n) if (!dfn[i]) tarjan(i);

  rep(u, 1, n) {
    val[col[u]]++;

    for (int i = head[u]; i; i = Next[i]) {
      int v = ver[i]; 
      if (col[u] != col[v]) g[col[u]].push_back(col[v]);
    }
  }

  rep(i, 1, cn) f[i] = -inf;
  f[col[c]] = 0; ans = 0;

  per(i, cn, 1) {
    f[i] += val[i];
    chkmax(ans, f[i]);
    for (int v : g[i]) chkmax(f[v], f[i]);
  }

  write(ans, '\n');
  rep(i, 1, n) head[i] = 0;
  rep(i, 1, cn) g[i].clear();
  rep(i, 1, n) low[i] = dfn[i] = 0;
  rep(i, 1, cn) val[i] = 0;
  rep(i, 1, n) col[i] = 0;
  dn = cn = 0, tot = 1;
}

int main() {
  read(T, id);
  while (T--) solve();
  return 0;
}

P4819 [中山市选] 杀人游戏

\(\color{#FFA500}{[NORMAL]}\)

缩点成 DAG,只用询问入度为 \(0\) 的节点。难的是存在入度为 \(0\) 的节点不用访问:如果存在一个或多个节点大小为 \(1\)所有出边连接的节点度数 \(\ge 2\),存在一个点是不用访问的,原因显然。

与度数有关,建 DAG 却不判重边,那我很唐了

code
const int N = 1e5 + 5;
const int M = 3e5 + 5;

struct Graph {
  int head[N], ver[M], Next[M], tot = 1, ind[N];
  
  void add(int u, int v) {
    Next[++tot] = head[u];
    ind[ver[head[u] = tot] = v]++;
  }
} G, DAG;

int n, m;
int low[N], dfn[N], ins[N], st[N], top, dn;
int cn, col[N], sz[N];
map<pair<int, int>, bool> mp;

void tarjan(int u) {
  low[u] = dfn[u] = ++dn, ins[u] = 1, st[++top] = u;
  
  adj(G, u, v) {
    if (!dfn[v]) tarjan(v), chkmin(low[u], low[v]);
    else if (ins[v]) chkmin(low[u], dfn[v]);
  }

  if (dfn[u] == low[u]) {
    col[u] = ++cn, ins[u] = 0;
    while (st[top] != u) col[st[top]] = cn, ins[st[top--]] = 0;
    top--;
  }
}

int main() {
  read(n, m);

  rep(i, 1, m) {
    int u, v; read(u, v);
    G.add(u, v);
  }

  rep(i, 1, n) if (!dfn[i]) tarjan(i);

  rep(u, 1, n) {
    sz[col[u]]++;
    adj(G, u, v) if (col[u] != col[v] && !mp[{col[u], col[v]}]) DAG.add(col[u], col[v]), mp[{col[u], col[v]}] = 1;
  }

  int cnt = 0; bool flag = 1;
  
  rep(u, 1, cn) {
    if (!DAG.ind[u]) {
      cnt++;

      if (flag && sz[u] == 1) {
        bool check = 1;
        adj(DAG, u, v) if (DAG.ind[v] == 1) { check = 0; break; }
        if (check) flag = 0, cnt--;
      } 
    }
  }

  printf("%0.6lf", (n - cnt) / (n * 1.0));
  return 0;
}

P5025 [SNOI2017] 炸弹

\(\color{#FFA500}{[NORMAL+]}\)

需要用线段树优化建图,建图后跑 tarjan 缩点形成 DAG。需要求某个节点能到达的其他节点的权值和。这是不可做的。

每个节点代表的一定是一段区间,维护每个节点代表的区间,最后求的是区间并,倒序拓扑就做完了。

如果你觉得自己很傻,那你可以想想我试图解决不可做问题。

code
const int N = 5e5 + 5;
const int M = 1.15e7 + 5;;
const int mod = 1e9 + 7;

struct Graph {
  int head[N << 2], ver[M], Next[M], tot = 1;

  void add(int u, int v) {
    Next[++tot] = head[u];
    ver[head[u] = tot] = v;
  }
} G, DAG;

struct node { int l, r; } a[N << 2], seg[N << 2];

int n;
i64 x[N], r[N], ans;
int R, ls[N << 2], rs[N << 2], node;
int low[N << 2], dfn[N << 2], dn, col[N << 2], cn, st[N << 2], top;
bool ins[N << 2];

void build(int &rt, int l, int r) {
  if (l == r) return rt = l, void();
  rt = ++node;
  int mid = (l + r) >> 1;
  build(ls[rt], l, mid), build(rs[rt], mid + 1, r);
  G.add(rt, ls[rt]), G.add(rt, rs[rt]);
}

void update(int rt, int l, int r, int L, int R, int v) {
  if (L <= l && r <= R) return G.add(v, rt), void();
  int mid = (l + r) >> 1;
  if (L <= mid) update(ls[rt], l, mid, L, R, v);
  if(mid < R) update(rs[rt], mid + 1, r, L, R, v);
}

void tarjan(int u) {
  low[u] = dfn[u] = ++dn; ins[u] = 1;
  st[++top] = u;
  
  adj(G, u, v) {
    if (!dfn[v]) tarjan(v), chkmin(low[u], low[v]);
    else if (ins[v]) chkmin(low[u], dfn[v]);
  }

  if (dfn[u] == low[u]) {
    col[u] = ++cn, ins[u] = 0;
    chkmin(seg[cn].l, a[u].l);
    chkmax(seg[cn].r, a[u].r);

    while (st[top] != u) {
      ins[st[top]] = 0;
      chkmin(seg[cn].l, a[st[top]].l);
      chkmax(seg[cn].r, a[st[top]].r);
      col[st[top--]] = cn;
    }

    top--;
  }
}

int main() {
#ifdef LOCAL
  freopen("code.in", "r", stdin);
  freopen("code.out", "w", stdout);
  auto start_time = chrono::high_resolution_clock::now();
#endif

  read(n); node = n;
  build(R, 1, n);
  rep(i, 1, n) read(x[i], r[i]);

  rep(i, 1, n) {
    int _L = lower_bound(x + 1, x + n + 1, x[i] - r[i]) - x,
        _R = upper_bound(x + 1, x + n + 1, x[i] + r[i]) - x - 1;
    a[i] = {_L, _R};
    update(R, 1, n, _L, _R, i);
  }

  rep(i, n + 1, node) a[i] = {n + 1, -1};
  rep(i, 1, node) seg[i] = {n + 1, -1};
  rep(i, 1, node) if (!dfn[i]) tarjan(i);
  rep(u, 1, node) adj(G, u, v) if (col[u] != col[v]) DAG.add(col[u], col[v]);

  rep(u, 1, cn) adj(DAG, u, v) {
    chkmin(seg[u].l, seg[v].l);
    chkmax(seg[u].r, seg[v].r);
  }

  rep(i, 1, n) (ans += 1LL * i * (seg[col[i]].r - seg[col[i]].l + 1)) %= mod;
  write(ans);
  return 0;
}

P8269 [USACO22OPEN] Visits S

\(\color{#39C5BB}{[EASY]}\)

可以发现只有强连通分量中有一条边无贡献,显然选最小的边。

code
const int N = 1e5 + 5;

struct Graph {
  int head[N], ver[N << 1], Next[N << 1], tot = 1;

  void add(int u, int v) {
    Next[++tot] = head[u];
    ver[head[u] = tot] = v;
  }
} G;

int n, val[N];
int low[N], dfn[N], dn, st[N], top, sz[N], cn, Min[N];
bool ins[N];
i64 ans;

void tarjan(int u) {
  dfn[u] = low[u] = ++dn, st[++top] = u;
  ins[u] = 1;

  adj(G, u, v) {
    if (!dfn[v]) tarjan(v), chkmin(low[u], low[v]);
    else if (ins[v]) chkmin(low[u], dfn[v]);
  }

  if (dfn[u] == low[u]) {
    cn++, sz[cn]++; ins[u] = 0; Min[cn] = val[u], ans += val[u];
    while (st[top] != u) ins[st[top]] = 0, sz[cn]++, ans += val[st[top]], chkmin(Min[cn], val[st[top--]]);
    top--;
  }
}

int main() {
  read(n);
  
  rep(i, 1, n) {
    int p; read(p, val[i]);
    G.add(i, p);
  }

  rep(i, 1, n) if (!dfn[i]) tarjan(i);
  rep(i, 1, cn) if (sz[i] > 1) ans -= Min[i];
  write(ans);
  return 0;
}
posted @ 2025-07-16 10:58  FRZ_29  阅读(13)  评论(0)    收藏  举报