图论
数组链表
struct ALG
{
int head[MAXN];
int to[MAXM], nxt[MAXM], val[MAXM];
int tot = 0;
void add_e(int u, int v, int w)
{
to[++tot] = v;
val[tot] = w;
nxt[tot] = head[u];
head[u] = tot;
}
};
树论
树的性质(直径,中心,重心)
- 若带有 负权边 则不可两遍 dfs 求直径(可dp求)。
- 若边权均为正,所有直径的中点重合。
- 树的中心不一定唯一,但最多有 2 个,一定位于树的直径上,且这两个中心是相邻的。
- 树上所有点到其最远点的路径一定交会于树的中心。
- 当通过在两棵树间连一条边以合并为一棵树时,连接两棵树的中心可以使新树的直径最小。
- 树的中心到其他任意节点的距离不超过树直径的一半。
- 当树以重心为根时,任一真子树的大小均不超过原树结点数的一半。
- 在所有以某个结点为根时的最大真子树大小中,以重心为根时所得到的值最小。
- 在所有以某个结点为根时所有结点的深度和中,以重心为根时深度和最小。
- 树的重心如果不唯一,则恰有 2 个.这两个重心相邻,且删去它们的连边后,树将变为两个大小相同的连通分量。
- 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
- 把两棵树通过一条边相连得到一棵新的树,那么新树的重心在连接原来两棵树的重心的路径上。
- 一棵有根树的重心一定在根结点所在的重链上.一棵树的重心一定是该树根结点重子结点对应子树的重心的祖先。
lca
// 树上倍增
struct ALG
{
int head[MAXN], val[MAXN];
int to[MAXM], nxt[MAXM];
int tot = 0;
int len = 0;
int sz[MAXN], dep[MAXN], top[MAXN];
int stjmp[MAXN][MAXJ];
void add_e(int u, int v)
{
to[++tot] = v;
nxt[tot] = head[u];
head[u] = tot;
}
void DFS(int f, int u)
{
sz[u] = 1;
dep[u] = dep[f] + 1;
stjmp[u][0] = f;
for (int i = 1; i < MAXJ; i++) stjmp[u][i] = stjmp[stjmp[u][i - 1]][i - 1];
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == f) continue;
DFS(u, v, rt);
sz[u] += sz[v];
}
}
int lca(int u, int v)
{
if (dep[u] < dep[v]) swap(u, v);
for (int i = MAXJ - 1; i >= 0; i--)
{
if (dep[stjmp[u][i]] >= dep[v]) u = stjmp[u][i];
if (dep[u] == dep[v]) break;
}
if (u == v) return v;
for (int i = MAXJ - 1; i >= 0; i--)
{
if (stjmp[u][i] != stjmp[v][i])
{
u = stjmp[u][i];
v = stjmp[v][i];
}
}
return stjmp[v][0];
}
}
// tarjan离线
int n, m, s;
struct ALG
{
// 原树
int head[MAXN];
int to[MAXM], nxt[MAXM];
int cnt = 0;
// 询问图
int qhead[MAXN];
int qto[MAXM], qnxt[MAXM], qid[MAXM];
int qcnt = 0;
int fa[MAXN];
bool vis[MAXN];
int ans[MAXN];
void add_e(int u, int v)
{
to[++cnt] = v;
nxt[cnt] = head[u];
head[u] = cnt;
}
void add_q(int u, int v, int id)
{
qto[++qcnt] = v;
qid[qcnt] = id;
qnxt[qcnt] = qhead[u];
qhead[u] = qcnt;
}
int find(int u)
{
if (fa[u] == u) return u;
return fa[u] = find(fa[u]);
}
void tarjan(int u)
{
vis[u] = true;
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (vis[v]) continue;
tarjan(v);
// v 的整棵子树处理完成后,
// 将 v 所在集合并到 u
fa[v] = u;
}
for (int i = qhead[u]; i; i = qnxt[i])
{
int v = qto[i];
if (vis[v])
{
ans[qid[i]] = find(v);
}
}
}
void init()
{
for (int i = 1; i <= n; i++)
{
fa[i] = i;
}
}
} g;
void solve()
{
cin >> n >> m >> s;
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
g.add_e(u, v);
g.add_e(v, u);
}
for (int i = 1, u, v; i <= m; i++)
{
cin >> u >> v;
g.add_q(u, v, i);
g.add_q(v, u, i);
}
g.init();
g.tarjan(s);
for (int i = 1; i <= m; i++)
{
cout << g.ans[i] << '\n';
}
}
重链剖分法见下
重链剖分
重链剖分往往出现在涉及路径上数据维护的题目中 重链数据合并时注意数据的方向,可以封装一个数据类,重载 + 用来表示数据的合并,在线段树里亦可以使用。
struct Decompose
{
int head[MAXN], val[MAXN];
int to[MAXM], nxt[MAXM];
int tot = 0;
int fa[MAXN], sz[MAXN], dep[MAXN], hev[MAXN];
int top[MAXN], dfn[MAXN], rnk[MAXN], stp;
void add_e(int u, int v)
{
to[++tot] = v;
nxt[tot] = head[u];
head[u] = tot;
}
void DFS1(int f, int u)
{
sz[u] = 1;
fa[u] = f;
dep[u] = dep[f] + 1;
int mv = 0;
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == f) continue;
DFS1(u, v);
sz[u] += sz[v];
if (sz[v] > sz[mv]) mv = v;
}
hev[u] = mv;
}
void DFS2(int u, int t)
{
top[u] = t;
dfn[u] = ++stp;
rnk[stp] = u;
if (hev[u]) DFS2(hev[u], t);
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == fa[u] || v == hev[u]) continue;
DFS2(v, v);
}
}
void build()
{
DFS1(0, 1);
DFS2(1, 1);
}
int lca(int u, int v)
{
while (top[u] != top[v])
{
if (dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
}
return (dep[u] < dep[v]) ? u : v;
}
} g;
长链剖分
注意长剖跳转根节点的复杂度是 下面是 LA 问题的模板
int high[MAXN]; // 预处理最高有效位 high[i] = high[i / 2] + 1;
struct Decompose
{
int head[MAXN];
int to[MAXM], nxt[MAXM];
int tot = 0;
int dep[MAXN], len[MAXN], son[MAXN];
int top[MAXN], dfn[MAXN], stp = 0;
int root;
int stjmp[MAXN][MAXJ];
int dw[MAXN], up[MAXN];
int val[MAXN];
void add_e(int u, int v)
{
to[++tot] = v;
nxt[tot] = head[u];
head[u] = tot;
}
void DFS1(int u)
{
dep[u] = dep[stjmp[u][0]] + 1;
for (int p = 1; p < MAXJ; p++) stjmp[u][p] = stjmp[stjmp[u][p - 1]][p - 1];
int mv = 0;
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
DFS1(v);
if (len[v] > len[mv]) mv = v;
}
son[u] = mv;
len[u] = len[mv] + 1;
}
void DFS2(int u, int t)
{
top[u] = t;
dfn[u] = ++stp;
dw[stp] = u;
if (!son[u]) return;
DFS2(son[u], t);
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == son[u]) continue;
DFS2(v, v);
}
}
void prepare()
{
DFS1(root);
DFS2(root, root);
for (int u = 1; u <= n; u++)
{
if (top[u] != u) continue;
for (int i = 0, v = u; v && i < len[u]; i++, v = stjmp[v][0])
{
up[dfn[u] + i] = v;
}
}
}
int query(int x, int k)
{
if (k == 0) return x;
int h = high[k];
if (k == 1 << h) return stjmp[x][h];
x = stjmp[x][h];
k -= 1 << h;
k -= dep[x] - dep[top[x]];
x = top[x];
return (k >= 0) ? up[dfn[x] + k] : dw[dfn[x] - k];
}
} g;
树上启发式合并
特征为静态数据查询,不支持修改操作。
小集合并大集合的思想更重要——每个点最多遍历 次,所以可以放心大胆遍历轻链,dsu on tree具备很灵活的数据维护能力。
可以与树dp混用,但要注意数据更新顺序,尤其注意需要跨子树合并答案时,一般会分开写两个递归函数,先维护答案,再维护动态数据。
struct DSU_on_tree
{
int head[MAXN];
int to[MAXM], nxt[MAXM], val[MAXM];
int tot = 0;
// 重链剖分
int fa[MAXN], sz[MAXN], dep[MAXN], hev[MAXN];
/* data */
void add_e(int u, int v, int w)
{
to[++tot] = v;
val[tot] = w;
nxt[tot] = head[u];
head[u] = tot;
}
void DFS1(int f, int u)
{
sz[u] = 1;
fa[u] = f;
int mv = 0;
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == f) continue;
dep[v] = dep[u] + 1;
DFS1(v);
sz[u] += sz[v];
if (sz[v] > sz[mv]) mv = v;
}
hev[u] = mv;
}
void effect(int u)
{
/* data maintenance */
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == fa[u]) continue;
effect(v);
}
}
void cancel(int u)
{
/* data maintenance */
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == fa[u]) continue;
cancel(v);
}
}
void DFS2(int u, int keep)
{
for (int i = head[u]; i ; i = nxt[i])
{
int v = to[i];
if (v == hev[u] || v == fa[u]) continue;
DFS2(v, 0);
}
if (hev[u]) DFS2(hev[u], 1);
/* data maintenance */
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (v == hev[u] || v == fa[u]) continue;
effect(v);
}
if (!keep) cancel(u);
}
} g;
虚树
每次询问仅涉及一棵树上的 k 个关键点,在整棵树上求解复杂度与总节点数 n 相关。 在复杂度允许范围内。
建树。(双排序和单调栈法复杂度相同)
经常可以看到一些典型的虚树题可以用 dsu on tree 解。
struct ImTree
{
int head[MAXN];
int nxt[MAXM], to[MAXM], tot = 0;
void add_e(int u, int v)
{
to[++tot] = v;
nxt[tot] = head[u];
head[u] = tot;
}
int vhead[MAXN];
int vnxt[MAXM], vto[MAXM], vtot = 0;
ll vwei[MAXM];
int vroot = 0;
void add_ve(int u, int v, ll w)
{
vto[++vtot] = v;
vwei[vtot] = w;
vnxt[vtot] = vhead[u];
vhead[u] = vtot;
}
int dep[MAXN], dfn[MAXN], stp = 0;
int stjmp[MAXN][MAXJ];
/*
* some data on virtual tree
*/
int key[MAXN], kcnt = 0;
bool iskey[MAXN];
void add_key(int u)
{
key[++kcnt] = u;
iskey[u] = true;
}
void clear_key()
{
for (int i = 1; i <= kcnt; i++) iskey[key[i]] = false;
kcnt = 0;
}
void DFS1(int f, int u)
{
dfn[u] = ++stp;
dep[u] = dep[f] + 1;
stjmp[u][0] = f;
for (int i = 1; i < MAXJ; i++) stjmp[u][i] = stjmp[stjmp[u][i - 1]][i - 1];
for (int e = head[u]; e ; e = nxt[e])
{
int v = to[e];
if (v == f) continue;
DFS1(u, v);
}
}
int LCA(int u, int v)
{
if (dep[u] < dep[v]) swap(u, v);
for (int i = MAXJ - 1; i >= 0; i--)
{
if (dep[stjmp[u][i]] >= dep[v]) u = stjmp[u][i];
if (dep[u] == dep[v]) break;
}
if (u == v) return v;
for (int i = MAXJ - 1; i >= 0; i--)
{
if (stjmp[u][i] != stjmp[v][i])
{
u = stjmp[u][i];
v = stjmp[v][i];
}
}
return stjmp[v][0];
}
void build()
{
auto cmp = [&] (const int &x, const int &y) { return dfn[x] < dfn[y];};
sort(key + 1, key + 1 + kcnt, cmp);
vector<int> nodes;
for (int i = 1; i < kcnt; i++)
{
nodes.emplace_back(key[i]);
nodes.emplace_back(LCA(key[i], key[i + 1]));
}
nodes.emplace_back(key[kcnt]);
sort(nodes.begin(), nodes.end(), cmp);
nodes.erase(unique(nodes.begin(), nodes.end()), nodes.end());
vtot = 0;
for (int u : nodes) vhead[u] = 0;
int sz = nodes.size();
for (int i = 1; i < sz; i++)
{
int f = LCA(nodes[i - 1], nodes[i]);
add_ve(f, nodes[i], dep[nodes[i]] - dep[f]);
}
vroot = nodes[0];
}
void DFS2(int u)
{
/*
* initialize or reset data
*/
for (int e = vhead[u]; e ; e = vnxt[e])
{
int v = vto[e];
DFS2(v);
/*
* dp or something else
*/
}
}
} g;
树分治
最短路
Dijkstra
Floyd
SPFA
生成树
Prim
struct EDGE
{
int to, val, nxt;
} e[MAXNODE << 1];
bool operator<(const EDGE &x, const EDGE &y) { return x.val < y.val; }
int head[MAXNODE], dis[MAXNODE];
bool vis[MAXNODE];
void add_edge(int u, int v, int w, int id)
{
e[id] = { v, w, head[u] };
head[u] = id;
}
int N, K, enemy[MAXNODE];
ll ans;
priority_queue<EDGE> q;
void Prim()
{
int tot = K;
for (int i = 1; i <= K; i++)
{
int rt = enemy[i];
for (int i = head[rt]; i ; i = e[i].nxt)
{
int ch = e[i].to, w = e[i].val;
if (!vis[ch] && w > dis[ch])
{
q.emplace(e[i]);
dis[ch] = w;
}
}
}
while (!q.empty())
{
if (tot >= N) break;
int u = q.top().to, w = q.top().val;
q.pop();
if (vis[u]) continue;
vis[u] = true;
tot++;
ans -= (ll)w;
for (int i = head[u]; i ; i = e[i].nxt)
{
int ch = e[i].to, w = e[i].val;
if (vis[ch]) continue;
if (w > dis[ch])
{
dis[ch] = w;
q.emplace(e[i]);
}
}
}
}
Kruskal
连通性
Tarjan缩点
void tarjan(int u)
{
low[u] = dfn[u] = ++stp;
stk[++stktop] = u, instk[u] = true;
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (instk[v])
{
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u])
{
tot++;
int k;
do
{
k = stk[stktop--];
instk[k] = false;
scc[k] = tot;
}while (k != u);
}
}
2-SAT
欧拉回路
这里是有向图的起点终点判定。
如果题目要求字典序最大/小,在建图前先对边进行排序。
int get_begin()
{
int beg = 0, end = 0;
for (int i = 1; i <= n; i++)
{
int dd = outdeg[i] - indeg[i];
if (dd < -1 || dd > 1 || (dd == 1 && beg) || (dd == -1 && end)) return 0;
if (dd == 1) beg = i;
else if (dd == -1) end = i;
}
if ((!beg) ^ (!end)) return 0;
if (beg) return beg;
for (int i = 1; i <= n; i++)
{
if (indeg[i]) return i;
}
return 0;
}
int ans[MAXN], cnt = 0;
void Hierholzer(int u)
{
for (int i = head[u]; i ; i = head[u])
{
head[u] = nxt[i]; // 链表头删节点(如果不想破坏图的结构可再copy一个head数组, 这样好维护)
Hierholzer(to[i]);
}
ans[++cnt] = u;
}