8月清北学堂培训 Day6
今天是杨思祺老师的讲授~
图论
双连通分量
在无向图中,如果无论删去哪条边都不能使得 u 和 v 不联通, 则称 u 和 v 边双连通;
在无向图中,如果无论删去哪个点(非 u 和 v)都不能使得 u 和 v 不联通,则称 u 和 v 点双连通。
u 到 v 的路径没有必经边和必经点。
割点:删去该点,图分裂为多个连通块。
割边:也叫 “ 桥 ”,删去该边,图分裂为多个连通块。
点双连通分量
类似地,定义 dfnu 和 lowu。 如果 v 是 u 的子结点,并且 lowv ≥ dfnu 则点 u 是割点,删去 点 u 后 v 子树和其它点不连通。 每个割点属于多个点双连通分量,非割点只属于一个点双连通分量。
边双连通分量
类似地,定义 dfnu 和 lowu。 如果 v 是 u 的子结点,并且 lowv > dfnu 则边 < u, v > 是割边。 每个点属于一个边双连通分量,边双连通分量之间以割边连接。
洛谷P3469 [POI2008]BLO-Blockade
在Byteotia有 n 个城镇。 一些城镇之间由无向边连接。 在城镇外没有十字路口,尽管可能有桥,隧道或者高架公路(反正不考虑这些)。每两个城镇之间至多只有一条直接连接的道路。人们可以从任意一个城镇直接或间接到达另一个城镇。 每个城镇都有一个公民,他们被孤独所困扰。事实证明,每个公民都想拜访其他所有公民一次(在主人所在的城镇)。所以,一共会有 n *(n-1)次拜访。
不幸的是,一个程序员总罢工正在进行中,那些程序员迫切要求购买某个软件。
作为抗议行动,程序员们计划封锁一些城镇,阻止人们进入,离开或者路过那里。
正如我们所说,他们正在讨论选择哪些城镇会导致最严重的后果。
编写一个程序:
读入Byteotia的道路系统,对于每个被决定的城镇,如果它被封锁,有多少访问不会发生,输出结果。
题解:
如果我们删去的不是割点,那么答案是 2 ( n-1 )(别人无法到自己,自己无法到别人);
那如果我们删去的是割点,那么答案就是每个点双联通分量的大小的乘积 。(规模最大为 O ( n ) )
POJ 3177
给定一个无向连通图,问添加几条边可以使它变成一个双连通图。
题解:
二分图
二分图
二分图:点黑白染色,邻点不同色。
二分图判定
如何判断一个给定的无向图是不是二分图?
从任意一点开始 BFS 或 DFS,起点不妨染色为白;
当前在 u 点时,尝试将所有邻点染为不同的颜色;
如果邻点已经染色且颜色不符,则不是二分图;
二分图的等价条件
无向图是二分图当且仅当其不包含奇环。
二分图匹配
匹配:选取一些边,使得任意两条边没有公共点(每个点至 多属于一条边);
最大匹配:选取边数尽可能多;
完美匹配:所有点都在匹配中(每个点恰好属于一条边) ;
匹配边:选中的边;
非匹配边:没有选中的边;
匹配点:和选中边相连的点;
非匹配点:和选中边不相连的点;
常常将二分图的点画成两列;
二分图最大匹配
二分图最大匹配是一个常见问题。
匈牙利算法
网络流
匈牙利算法
理论基础 :
交错路:从非匹配点出发,依次经过非匹配边、匹配边、非匹配边 ... ;
非匹配边恰好比匹配边多一个;然后我们交换非匹配边和匹配边就是使答案更优;
增广路:从非匹配点出发,结束于非匹配点的交错路;
增广路定理:任意一个非最大匹配的匹配一定存在增广路;
网络流
取额外的两个点作为源点和汇点;
源点向左边一列每个点连流量为 1 的边;
右边一列每个点向汇点连流量为 1 的边;
二分图中每条边从左向右连流量为 1 的边;
求最大流即可 。
网络流的最小割等于最大流。
最小顶点覆盖 Knoig 定理
二分图最小顶点覆盖数等于其最大匹配数。
POJ 3041 Asteroids
一个 N × N 的网格中,有 K 个大小为 1 × 1 的小行星,现在可以用激光枪每次消灭一行的小行星或者消灭一列的小行星。问最少需要使用多少次激光枪消灭所有的小行星。
题解:
每行为左边一点;
每列为右边一点;
每个小行星为一边;
选择最少的点覆盖所有边;
最小路径覆盖
给定有向图 G < V, E >。设 P 是 G 的一个简单路(顶点不相交)的集合。如果 V 中每个顶点恰好在 P 的一条路上,则称 P 是 G 的一个路径覆盖。P 中路径可以从 V 的任何一个顶点开始, 长度也是任意的,特别地,可以为 0 。G 的最小路径覆盖是 G 的 所含路径条数最少的路径覆盖。
最小路径覆盖 = |V| - 二分图最大匹配
二分图:将原图每个点拆分为入点和出点,如果原图存在 u 到 v 的边,则在 u 的出点和 v 的入点间连无向边。
BZOJ 2150 部落战争
lanzerb 的部落在 A 国的上部,他们不满天寒地冻的环境,于是准备向 A 国的下部征战来获得更大的领土。A 国是一个 M × N 的矩阵,其中某些地方是城镇,某些地方是高山深涧无人居住。 lanzerb 把自己的部落分成若干支军队,他们约定:
1. 每支军队可以从任意一个城镇出发,并只能从上往向下征战,途中只能经过城镇,可以在任意一个城镇停止征战。
2. 每个城镇只能被一支军队经过。
3. 行军方式类似国际象棋中的马,不过只能走 R × C 的路线。
lanzerb 的野心使得他的目标是统一全国,但是兵力的限制使得他们在配备人手时力不从心。假设他们每支军队都能顺利占领这支军队经过的所有城镇,请你帮 lanzerb 算算至少要多少支军队 才能完成统一全国的大业。
题解:
最小路径覆盖问题,转化成求最大二分图匹配。
差分约束
差分约束可以确定特定的不等式组是否存在解。
xi1 − xj1 ≤ a1
xi2 − xj2 ≤ a2
. . .
为每个变量 xi 建立一个点 pi。 # 如果要求 xi − xj ≤ a,则建立一条从 pj 到 pi 长度为 a 的边;
以任意一点为起点求单源最短路,则一定有 di − dj ≤ a 如果出现负环,则归结出形如 xi − xi < 0 的约束,不等式组无解。 如果没有负环,最短路算法得到的距离数组就是一组合法的解。
BZOJ 2330 糖果
幼儿园里有 N 个小朋友,lxhgww 老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。但是小朋友们也有嫉妒心,总是会提出一些要求,比如小明不希望小红分到的糖果比他的多,于是在分配糖果的时候,lxhgww 需要满足小朋友们的 K 个要求。幼儿园的糖果总是有限的,lxhgww 想知道他至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。 N ≤ 100000, K ≤ 100000 。
每个限制为三个整数:X, A, B。
如果 X = 1,表示第 A 个小朋友分到的糖果必须和第 B 个小 朋友分到的糖果一样多;
如果 X = 2,表示第 A 个小朋友分到的糖果必须少于第 B 个 小朋友分到的糖果;
如果 X = 3,表示第 A 个小朋友分到的糖果必须不少于第 B 个小朋友分到的糖果;
如果 X = 4,表示第 A 个小朋友分到的糖果必须多于第 B 个 小朋友分到的糖果;
如果 X = 5,表示第 A 个小朋友分到的糖果必须不多于第 B 个小朋友分到的糖果;
题解:
易见每种限制都可以表示为差分约束形式的不等式。 因为每个小朋友得到的糖果数至少为 1,要求最少总糖果数,所 以设置超级源点,向每个点连长度为 1 的边,跑单源最长路即可。
BZOJ 1202 狡猾的商人
***姹接到一个任务,为税务部门调查一位商人的账本,看看账本 是不是伪造的。账本上记录了 n 个月以来的收入情况,其中第 i 个月的收入额为 Ai ( i = 1,2,3...n-1,n),。当 Ai 大于 0 时表示这个 月盈利 Ai 元,当 Ai 小于 0 时表示这个月亏损 Ai 元。所谓一段时间内的总收入,就是这段时间内每个月的收入额的总和。***姹的任务是秘密进行的,为了调查商人的账本,她只好跑到商人那里打工。她趁商人不在时去偷看账本,可是她无法将账本偷出来,每次偷看账本时她都只能看某段时间内账本上记录的收入情况,并且她只能记住这段时间内的总收入。现在,***姹总共偷看了 m 次账本,当然也就记住了 m 段时间内的总收入,你的任务是根据记住的这些信息来判断账本是不是假的。
题解:
以 Si 表示 Ai 的前缀和,则每个限制形如 Su − Sv = k,将其拆分为两个不等式:
Su − Sv ≤ k
Su − Sv ≥ k 即 Sv − Su ≤ −k 差分约束后如果出现负环,则信息有假(会出现 Si - Si < 0 的情况)。
当然,对于这种全部为等式的差分约束问题,用 DFS 或 BFS 判 断即可,不需要应用最短路算法。
BZOJ 4500 矩阵
有一个 n × m 的矩阵,初始每个格子的权值都为 0,可以对矩阵 执行两种操作:
1. 选择一行,该行每个格子的权值加 1 或减 1。
2. 选择一列,该列每个格子的权值加 1 或减 1。 现在有 K 个限制,每个限制为一个三元组 ( x , y , c ),代表格子 ( x , y ) 权值等于 c。问是否存在一个操作序列,使得操作完后的矩 阵满足所有的限制。如果存在输出 ” Yes ”,否则输出 ” No ”。
题解:
建立 n 个点,a1, a2, . . . , an,代表每一行的操作效果。 建立 m 个点,b1, b2, . . . , bm,代表每一列的操作效果。 对于限制 ( x , y , c ),强制 ax + by = c 即可,可以视为一条边。
DFS 或 BFS 判断即可。
下午考试:
T1
10 pts:
N3 暴力求出任意两点间的最短路,然后再 k2 枚举求出最短的距离即可;
50 pts:
注意到 k 的值比较小(k 如果大了的话跑最短路时走两步就遇到了党员所在的城市),所以我们可以跑 k 次 Dijkstra;
70 pts:
看到题目中的保证输入的无向图无环,狡猾的 ysq 故意设了一个坑:只是说没有环,又没有说是棵树,还有可能是一颗森林!
但是有 10% 真的是一棵树。。。
考虑用树形 dp,还是入门级别的,代码类似于树上求直径;
void dfs(int u) { f[u] = flag[u] ? 0 : inf; for (auto v : edge[u]) { ans = min(ans, f[v]); f[u] = min(f[u], f[v] + w[u][v]); } }
100 pts:
答案所连得两个点一定是不同的点,两个不同的编号,它们的二进制表示中一定有一位是不同的,那么我们可以去枚举不同位是第几位;
假如我们现在已经枚举到了第 i 位,将第 i 位是 1 的编号放到第一个集合里,将第 i 位是 0 的编号放到第二个集合里(这样的话可以保证从第一个集合到第二个集合的编号一定不相同),那么我们再跑一次从第一个集合到第二个集合的多源多汇最短路即可 。
考虑到 int 最多 32 位,所以我们最多也只需要跑 32 次最短路 。
怎么跑多源多汇最短路?
我们设一个超级原点 s 和一个超级汇点 t :
超级原点 s 向第一个集合的点连一条长度为 0 的边,第二个集合的点向超级汇点 t 连一条长度为 0 的边,我们只需跑超级原点 s 到超级汇点 t 的最短路就行了。
#include <queue> #include <cstdio> #include <cstring> template <class cls> inline cls min(const cls & a, const cls & b) { return a < b ? a : b; } const int mxn = 100005; const int mxm = 500005; const int inf = 0x3f3f3f3f; int n, m, k; int points[mxn]; int tot; int hd[mxn]; int nt[mxm]; int to[mxm]; int vl[mxm]; inline void add_edge(int u, int v, int w) { nt[++tot] = hd[u]; to[tot] = v; vl[tot] = w; hd[u] = tot; } int dis[mxn]; struct data { int u, d; data(int _u, int _d) : u(_u), d(_d) {} bool operator < (const data & that) const { return d > that.d; } }; std::priority_queue<data> heap; int main() { int cas; scanf("%d", &cas); for (int c = 0; c < cas; ++c) { scanf("%d%d%d", &n, &m, &k); memset(hd, 0, sizeof(int) * (n + 5)); tot = 0; for (int i = 0, u, v, w; i < m; ++i) { scanf("%d%d%d", &u, &v, &w); add_edge(u, v, w); add_edge(v, u, w); } for (int i = 0; i < k; ++i) scanf("%d", points + i); int ans = inf; for (int i = 1; i < k; i <<= 1) { memset(dis, inf, sizeof(int) * (n + 5)); for (int j = 0, p; j < k; ++j) if (p = points[j], (j & i) == 0) heap.push(data(p, dis[p] = 0)); while (!heap.empty()) { int u = heap.top().u; int d = heap.top().d; heap.pop(); if (dis[u] != d) continue; for (int e = hd[u], v, w; e; e = nt[e]) if (v = to[e], w = vl[e], dis[v] > d + w) heap.push(data(v, dis[v] = d + w)); } for (int j = 0, p; j < k; ++j) if (p = points[j], (j & i) != 0) ans = min(ans, dis[p]); } printf("%d\n", ans == inf ? -1 : ans); } return 0; }
T2
50 pts:
Tarjan 缩点,每个强连通分量里面维护一个所有点的最大值,然后就变成了一个 DAG,对于每次询问,我们只需要从起点的强连通分量里面跑一边 dfs 找出最大的值就好了;
不过这里我 O(n)先预处理了一下每个强连通分量所能到达的其他强连通分量的最大值,对于每次询问O(1)给出答案,不过一样是 50 pts;
100 pts:
场外 rqy 的神仙思路:
我们可以倒着想想:我们要求一个点往下走所经过的点的最大值,那么最大值的点反正走上去所经过的点的答案就是这个最大值的点的权值!
那么我们可以反着建边,然后再按照每个点的点权从大到小排序,从每个点开始 dfs 一遍,所经过的点的答案赋值为起点的权值;如果找到一个点被更新过了,那就停止往下搜索,返回即可;
时间复杂度?
n 次 dfs 保证了每个点至多被更新一次,那么 n 次dfs 的总时间复杂度就是 O(n),考虑排序的复杂度,那么这个题的复杂度就是 O ( nlog n )!快的飞起!
ysq:妙啊!我的标出被踩爆了。
#include <cstdio> #include <cstring> template <class cls> inline cls min(const cls & a, const cls & b) { return a < b ? a : b; } template <class cls> inline cls max(const cls & a, const cls & b) { return a > b ? a : b; } const int mxn = 200005; const int mxm = 400005; int n, m, k, w[mxn]; struct edge { int u, v; } edges[mxm]; int tot; int hd[mxn]; int to[mxm << 1]; int nt[mxm << 1]; inline void add_edge(int u, int v) { nt[++tot] = hd[u]; to[tot] = v; hd[u] = tot; } int tim; int cnt; int top; int dfn[mxn]; int low[mxn]; int stk[mxn]; int scc[mxn]; void tarjan(int u) { dfn[u] = low[u] = ++tim; stk[++top] = u; for (int e = hd[u], v; e; e = nt[e]) if (v = to[e], scc[v] == 0) { if (dfn[v] == 0)tarjan(v), low[u] = min(low[u], low[v]); else low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { cnt += 1; do { scc[stk[top]] = cnt; } while (stk[top--] != u); } } int oe[mxn]; int mx[mxn]; int que[mxn]; void bfs() { int l = 0, r = 0; for (int i = 1; i <= cnt; ++i) if (oe[i] == 0) que[r++] = i; while (l < r) { int u = que[l++]; for (int e = hd[u], v; e; e = nt[e]) if (v = to[e], mx[v] = max(mx[v], mx[u]), --oe[v] == 0) que[r++] = v; } } int main() { int cas; scanf("%d", &cas); for (int c = 0; c < cas; ++c) { scanf("%d%d%d", &n, &m, &k); for (int i = 1; i <= n; ++i) scanf("%d", w + i); memset(hd, 0, sizeof(int) * (n + 5)); tot = 0; for (int i = 0; i < m; ++i) { scanf("%d%d", &edges[i].u, &edges[i].v); add_edge(edges[i].u, edges[i].v); } tim = cnt = top = 0; memset(scc, 0, sizeof(int) * (n + 5)); memset(dfn, 0, sizeof(int) * (n + 5)); for (int i = 1; i <= n; ++i) if (scc[i] == 0) tarjan(i); memset(hd, 0, sizeof(int) * (cnt + 5)); tot = 0; memset(oe, 0, sizeof(int) * (cnt + 5)); memset(mx, 0, sizeof(int) * (cnt + 5)); for (int i = 0; i < m; ++i) { int u = scc[edges[i].u]; int v = scc[edges[i].v]; if (u != v) add_edge(v, u), oe[u] += 1; } for (int i = 1; i <= n; ++i) mx[scc[i]] = max(mx[scc[i]], w[i]); bfs(); for (int i = 0, u, x; i < k; ++i) { scanf("%d%d", &u, &x); printf("%lld\n", 1LL * x * mx[scc[u]]); } } return 0; }
T3
我们看到 20% 的数据颜色种类数不超过 5,这在提示我们建 5 棵线段树来维护每个颜色鸭;
如果一个结点 x 的颜色从 a -> b,那么我们在第 a 棵线段树里面减一,第 b 棵线段树里面加一就好了;
那么如果颜色种类数为 100 呢?
暴力做法:不管他,先开 100 棵线段树再说,维护的话和上面的一样。
我们可以在线段树里面开一个 100 大小的数组,记录每个区间结点有多少个第一种颜色的,有多少个第二种颜色的。。。有多少个第 100 种颜色的;
其他的操作都和线段树差不多吧,只是合并父亲结点的信息时我们可以 for 从 1 ~ 100 ,对应颜色数相加来得到父亲的信息;
我们还可以优化:
因为每次都要从 1 for 到 100,时间复杂度乘了个大常数,总时间复杂度:O(100n);
其实我们不用从 1 for 到 100,我们可以将区间 for 一遍啊,这样的话 for 一次的时间复杂度就是区间长度了;
考虑时间复杂度:
长度为 n 的序列,最多分成 log n 层,每层的若干区间相加的长度都是 n,也就是说每层的 for 循环的时间复杂度是 O(n),那么一共 O(n log n);
正解:
题目又没有强制在线,你可以离线做。
我们只需要建一棵线段树。
我们去枚举颜色,假设现在我们枚举到了颜色 a,那么这棵线段树就维护了 a 颜色的信息;(也就是说这棵线段树要时刻变化)
我们去枚举所有有关 a 颜色的所有操作(将 a -> b,将 b -> a,查询 x ~ y 中 a 颜色的个数),依次操作,将询问答案存起来,最后再输出;
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; inline int getint() { int r = 0, c = getchar(); for (; c < 48; c = getchar()); for (; c > 47; c = getchar()) r = r * 10 + c - 48; return r; } const int mxc = 100005; const int mxn = 100005; const int mxm = 200005; int n, m, c; int tt; int hd[mxn]; int to[mxm]; int nt[mxm]; inline void addedge(int x, int y) { nt[++tt] = hd[x], to[tt] = y, hd[x] = tt; nt[++tt] = hd[y], to[tt] = x, hd[y] = tt; } struct data { int k, x, y; data() {} ; data(int a, int b, int c) : k(a), x(b), y(c) {} ; }; int color[mxn]; #include <vector> vector<data> vec[mxc]; int tim; int dfn[mxn]; int top[mxn]; int fat[mxn]; int dep[mxn]; int son[mxn]; int siz[mxn]; void dfs1(int u, int f) { siz[u] = 1; son[u] = 0; fat[u] = f; dep[u] = dep[f] + 1; for (int i = hd[u], v; i; i = nt[i]) if (v = to[i], v != f) { dfs1(v, u); siz[u] += siz[v]; if (siz[v] > siz[son[u]]) son[u] = v; } } void dfs2(int u, int f) { dfn[u] = ++tim; if (son[f] == u) top[u] = top[f]; else top[u] = u; if (son[u]) dfs2(son[u], u); for (int i = hd[u], v; i; i = nt[i]) if (v = to[i], v != f && v != son[u]) dfs2(v, u); } int bit[mxn]; inline void add(int p, int v) { for (; p <= n; p += p & -p) bit[p] += v; } inline int ask(int l, int r) { int sum = 0; --l; for (; r; r -= r & -r) sum += bit[r]; for (; l; l -= l & -l) sum -= bit[l]; return sum; } int ans[mxn]; signed main() { int cas = getint(); while (cas--) { n = getint(); m = getint(); for (int i = 1; i <= n; ++i) vec[color[i] = getint()].push_back(data(0, i, +1)); c = 0; for (int i = 1; i <= n; ++i) c = max(c, color[i]); memset(hd, 0, sizeof(int) * (n + 5)); tt = 0; for (int i = 1; i < n; ++i) { int x = getint(); int y = getint(); addedge(x, y); } for (int i = 1; i <= m; ++i) { if (getint() == 1) { int p = getint(); int a = color[p]; int b = color[p] = getint(); vec[a].push_back(data(0, p, -1)); vec[b].push_back(data(0, p, +1)); } else { int x = getint(); int y = getint(); int k = getint(); vec[k].push_back(data(i, x, y)); } } dfs1(1, 0); dfs2(1, 0); memset(ans, -1, sizeof ans); for (int k = 1; k <= c; ++k) { int sz = vec[k].size(); memset(bit, 0, sizeof bit); for (int i = 0; i < sz; ++i) { const data &d = vec[k][i]; ans[d.k] = 0; if (d.k == 0) add(dfn[d.x], d.y); else { int a = d.x, ta = top[a]; int b = d.y, tb = top[b]; while (ta != tb) { if (dep[ta] >= dep[tb]) ans[d.k] += ask(dfn[ta], dfn[a]), ta = top[a = fat[ta]]; else ans[d.k] += ask(dfn[tb], dfn[b]), tb = top[b = fat[tb]]; } if (dep[a] <= dep[b]) ans[d.k] += ask(dfn[a], dfn[b]); else ans[d.k] += ask(dfn[b], dfn[a]); } } } for (int i = 1; i <= m; ++i) if (ans[i] >= 0) printf("%d\n", ans[i]); for (int i = 1; i <= c; ++i) vec[i].clear(); tim = 0; } return 0; }