图论随笔
随笔
图论
最短路
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;
}
线段树优化建图
处理向区间连边时有效的算法,线段树上的点相当于虚拟节点,象征一段区间所有点。让虚拟节点访问真实的点是简单的,把线段树上节点与左右儿子的连接当成有向边,叶子节点建成真节点即可。
根据线段树上的方向可以把线段树分为两类:
-
出树:从节点到儿子,真实节点连边是从真实节点连向虚拟节点,象征真实节点向一段区间的所有真实节点连有向边
-
入树:与出树完全相反
// 建出树模板
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\) 条边,但是没有这么强的数据。
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;
}
浙公网安备 33010602011771号