图论例题
ARC092F
考虑一条边连接的两个点,\(u, v\) 删除后一共有 4 种情况:
1: \(u\) 能到 \(v\),\(v\) 能到 \(u\)
2: \(u\) 不能到 \(v\),\(v\) 不能到\(u\)
3: \(u\) 能到 \(v\),\(v\) 不能到\(u\)
4: \(u\) 不能到 \(v\),\(v\) 能到 \(u\)
讨论一下发现情况 3,4 \(scc\) 数量会变化
问题就是怎么样在 \(O(nm)\) 的时间复杂度内求出连接情况
把每个点的每条边排序,做一个前缀和后缀表示点 \(u\) 能到的点,连起来就行了
比如说求前缀每次只会新加入一条边,如果碰到一个已经访问的点,那就不用再走了
每个点都整一遍,时间复杂度 \(O(nm)\)
404_NotFound
-
题意:给定 \(n\) 个点 \(m\) 条边的有向图,求每条边 \((u, v)\) 删除后 \(u\) 到 \(v\) 的最短路 \(n \le 1000, m \le 1e5\) 边权为 1
首先对每个点建出最短路树,考虑连接点 \(u\) 的边 \((u, v)\) 如果不是树边,那么去掉它肯定没影响,否则会影响到子树内点的最短路,子树外的不会影响 废话
考虑遍历子树外的所有点,会给子树内的点一个初值,用这个初值和子树内原本的点进行 \(bfs\) 就可以得到答案,这里因为边权很小,不会超过 \(n\),可以用 \(n\) 个队列模拟栈,点的初值可以遍历一遍一块处理
时间复杂度 \(O(nm)\)
HDU 6403
每张牌反面数字向正面数字连边,那每个数字最多入度为一,可以发现,除了树和奇环树其他图都做不到
先说奇环树,环要么顺时针要么逆时针,遍历判断一下就行了
再说树,假定一个点做根的话,那只有一个儿子能指向他,而且当儿子一但没有指向父亲,下面也就确定了,我们发现,整个图一定有一个点,指向所有儿子,换根DP一下,记录最优值出现次数
答案就是所有情况乘起来
JSOI 2018 绝地反击
二分答案,每个点对应到了圆上的一段弧,考虑往圆上放等距离的n个点,那验证答案合理性就是一个最大匹配
考虑最优解的情况,我们可以将所有点旋转,直到有一个点卡到圆弧边界,这时答案是不变的,也就是说,只需要验证n个点的其中一个在圆弧边界上时的答案
因为 \(n\) 个点是一样的,所以我可以个每个点分配一个区间,\(\frac{2\pi} n, \frac{2\pi * 2} n...\)
在逐渐旋转的过程中,每次会有一个点进入或走出一段圆弧,对应到网络流上,就是加入 \(or\) 删除一条边,加入直接加就行了,删除需要退一下流,这样做以后只需要再跑一次 \(Dinic\) 即可
时间复杂度 \(O(n^2\sqrt n\ log(n))\),难点在求圆弧
404_NotFound
-
题意:给定 \(n\) 组形如 \(((a, b), (c, d))\) 的数字,保证 \(a != b, c != d\) 先手在其中选出若干对,至少一对,满足同一组的一对不被同时选。 后手在先手选择的若干对中再选若干对,若每个数字在这些对中出现的次数是偶数,则后手胜,否则先手胜 \(n \le 300, T \le 20\)
都说是图论了,那选择 \((a, b)\) 就是连边 \(a -> b\),如果有环则后手胜
考虑像匈牙利算法那样扩展,加入一个新组,如果以前的组中通过切换边可以避免成环,则加入,否则加不了
考虑匈牙利算法的正确性,左边的点一旦匹配上了右边的点,则左边这个点以后一直不会被放弃。
这是因为一个边连接两个点,假设原最优解不包含这个点,则使用这个点增广后,会放弃最优解中的一个点,-1,+1,诶,还是最有解。而且不可能出现点 \(a\) 顶替掉点 \(b\) 点 \(c\) 又顶替掉电 \(a\) 的情况,因为 \(b\),\(c\)冲突不可能同时选
这个扩展方法和匈牙利很类似,他们的正确性都源于,匹配冲突时,可以去掉原来的一个匹配变得不冲突,使得最终解包含当前匹配
总结:当逐渐加入点冲突时,可以去掉一个点使得不冲突,那么就可以这么匹配
时间复杂度(暴力冲建图,暴力找环) \(O(Tn^3)\)
优化一下,用个 \(LCT\) 复杂度就变成 \(O(Tn^2logn)\) 妙
HDU 6431
给定 \(n\) 个点, \(m\) 条边的无向图,求
\(n, m \le 5e5\)
考虑最大流等于最小割,也就是求互不相交的联通路径个数
考虑 \(nm\) 的做法 那就是分别求出最小割是 \(1, 2, \ge 3\) 的路径个数
-
考虑求1:
\(tarjan\) 求出边双联通分量,哪任意两个边双中的点的最小割都是1 -
考虑求2:
考虑在边双里面断掉一条边,这时如果两个点不在同一个边双了,那它的最小割是2 -
考虑求3:
不是上面两种情况就是3
上面这个方法虽然愚蠢,但是给了我们一点思路:分别求出1,2,3就是剩下的
在边双连通分量里面建一棵 \(dfs\) 树,考虑树边和其他边的关系,在这些边集中,如果有两个点断掉两条边就不连通的话,那他们的最小割是 2
- \(dfs\) 树特性,所有非树边都是前向边(显然)
断掉的边有 3 种情况
1:非树边和非树边
2:树边和非树边
3:树边和树边
-
考虑1:
显然选了还联通,没用 -
考虑2:
假如只有一条非树边覆盖了这条树边,那么这条边连接的上下两个部分的最小割就是 2,我们会得到一堆这种边,做的时候顺便容斥一下就行了 -
考虑3:
思考什么时候可以割掉两条树边,那就是所有非树边要么覆盖两条树边,要么不覆盖两条树边,一旦有一条非树边只覆盖了一条边,那么割掉这两条树边就没有用了 -
实现:
基操,给每条边一个随机权值,通过非树边覆盖边的异或和判断 -
tips:
显然,2 和 3 有重复的部分,那怎么样 DP 不会重复呢
在 \(dfs\) 的过程中用 \(hash\) 记录每种权值的最后的边是谁,记录子树内没有和其他点割开的点,如果当前边是情况 2,那么把这条树边删了,也就是把这个树的子树中未割开的点和其他点割开,顺便统计答案。如果当前点是情况 2,那么把两条树边中间夹着的未割开的点删除,发现永远不可能出现两组 \(hash\) 相同的交叉的边,那记录一下没和当前点割开的点就行了
没了,简单吗?
发现拿了个 \(Rank\ 1\) 那还是贴下代码吧
// #include <bits/stdc++.h>
#include <tr1/unordered_map>
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <cmath>
#pragma GCC optimize(2)
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <cstdlib>
using namespace std;
using namespace std :: tr1;
#define rg register
char ss[1 << 17], *A = ss, *B = ss;
inline char gc(){ if(A == B){ B = (A = ss) + fread(ss, 1, 1 << 17, stdin); if(A == B) return EOF; } return *A++; }
//#define gc getchar
#define rep(i, a, b) for(int i = a; i <= b; ++i)
inline int read(){
rg char ch = gc();
rg int x = 0, f = 0;
while(!isdigit(ch)) f |= (ch == '-'), ch = gc();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = gc();
return f ? -x : x;
}
const int N = 1e5 + 5, M = 6e5 + 5;
int head[N], nxt[M], ver[M], tot, sz[N];
int f[N], f_sz[N], pos[N];
int find(int x){
return f[x] ? f[x] = find(f[x]) : x;
}
inline void merge(int x, int y){
int xx = find(x); int yy = find(y);
if(xx == yy) return;
if(pos[xx] < pos[yy]){
f_sz[yy] += f_sz[xx];
f[xx] = yy;
}else{
f_sz[xx] += f_sz[yy];
f[yy] = xx;
pos[xx] += (pos[xx] == pos[yy]);
}
}
inline void add(int x, int y){
ver[++tot] = y;
nxt[tot] = head[x];
head[x] = tot;
}
int sccnum[N], scccnt, dfn[N], low[N], cnt;
int s[N], stop;
int tt;
template <typename T> inline void ckmin(T &a, const T &b){
if(a > b) a = b;
}
#define ll long long
ll ans;
void tarjan(int x, int f){
dfn[x] = low[x] = ++cnt;
s[++stop] = x;
for(int i = head[x]; i; i = nxt[i]){
if(i == f) continue;
int y = ver[i];
if(!dfn[y]){
tarjan(y, i ^ 1);
ckmin(low[x], low[y]);
} else ckmin(low[x], dfn[y]);
}
if(dfn[x] == low[x]){
++scccnt;
sz[scccnt] = 0;
do{
sccnum[s[stop]] = scccnt;
++sz[scccnt];
}while(s[stop--] ^ x);
}
}
namespace Tree{
int head[N], nxt[M], g[N], ver[M], tot, vis[N], dep[N], fa[N], sum[N], link[M];
int n, st, ins[M], intot, inpoint[N], inp_cnt;
ll ans, f[N];
unordered_map<ll, int> mp;
inline void add(int x, int y){
ver[++tot] = y;
nxt[tot] = head[x];
head[x] = tot;
}
inline void prepro(int _n){
rep(t, 1, inp_cnt){
int i = inpoint[t];
head[i] = 0, g[i] = sum[i] = vis[i] = f[i] = 0;
}
unordered_map<ll, int>().swap(mp);
rep(i, 1, intot) link[ins[i]] = 0;
inp_cnt = 0;
tot = 1; intot = 0;
n = _n;
}
void build(int x, int f){
vis[x] = true;
fa[x] = f;
inpoint[++inp_cnt] = x;
for(int i = ::head[x]; i; i = ::nxt[i]){
int y = ::ver[i];
if(sccnum[y] ^ sccnum[x]) continue;
ins[++intot] = i;
if(vis[y]) continue;
add(x, y); add(y, x);
dep[y] = dep[x] + 1;
link[i] = link[i ^ 1] = true;
build(y, x);
}
}
void dfs(int x){
sum[x] = 1;
for(int i = head[x]; i; i = nxt[i]){
int y = ver[i];
if(y == fa[x]) continue;
dfs(y);
f[x] ^= f[y];
sum[x] += sum[y];
g[x] += g[y];
}
if(x == st) return;
if(g[x] == 1){
ans += 1ll * sum[x] * (n - sum[x]);
n -= sum[x];
sum[x] = 0;
}else if(mp.count(f[x])){
int y = mp[f[x]];
ans += 1ll * (sum[x] - sum[y]) * (sum[y] + n - sum[x]);
n -= (sum[x] - sum[y]);
sum[x] = sum[y];
}
mp[f[x]] = x;
}
inline ll solve(int s){
prepro(sz[sccnum[s]]);
dep[s] = 1;
build(s, 0);
ans = 0;
rep(t, 1, intot){
int i = ins[t];
int x = ::ver[i], y = ::ver[i ^ 1];
if(!link[i] && i < (i ^ 1)){
ll val = 10000000000000ll * rand() + 10000000000ll * rand() + 100000ll * rand() + rand();
if(dep[x] > dep[y]) swap(x, y);
f[y] ^= val; f[x] ^= val;
++g[y]; --g[x];
}
}
st = s;
dfs(s);
return ans;
}
}
int n, m, v[N];
inline void Main(){
rep(i, 1, n) pos[i] = f[i] = v[i] = head[i] = dfn[i] = 0; tot = 1; cnt = 0; scccnt = 0;
stop = 0; ans = 0;
n = read(), m = read();
rep(i, 1, n) f_sz[i] = 1;
rep(i, 1, m){
int x = read(), y = read();
add(x, y); add(y, x);
merge(x, y);
}
rep(i, 1, n) if(!dfn[i]) tarjan(i, 0);
ans = 0;
ll total = 0, all = 0;
rep(i, 1, n) if(!f[i]) all += (ll) f_sz[i] * (f_sz[i] - 1);
rep(i, 1, n){
if(v[sccnum[i]]) continue;
v[sccnum[i]] = true;
int x = find(i);
ans += (f_sz[x] - sz[sccnum[i]]) * (ll) sz[sccnum[i]];
total += Tree :: solve(i);
}
total *= 2;
ans += (all - ans - total) * 3 + total * 2;
printf("%lld\n", ans >> 1);
}
signed main(){
srand(23333);
int T = read();
while(T--) Main();
gc(), gc();
return 0;
}
TopCoder SRM 726 Div.1 Hard
有 \(n\) 个 \(DDL\) 每个 \(DDL\) 完成需要一天,可以在 \([L_i, R_i]\) 天中完成,完成后有 \(C_i\) 收益,求最大总收益 \(n \le 2e6, 1 \le L_i \le 1000, R_i \le L_i + 1000\)
考虑贪心,限制太多了,不好贪,但是这明显是个费用流,考虑模拟费用流
费用流的过程就是每次 \(spfa\) 出来一个最大最大值,可以把它看成一个匹配问题,从大到小排序,每次我要选择一个最大的值,看它能不能匹配进去
考虑匈牙利,发现每个点连接的是一段区间,如果连边的话边的个数是 \(m^2\) 的,但是只有 \(m\) 个点,那可以用一个 \(set\) 维护 \(vis\) 数组
每次 \(lower\_bound\) 一下就能找到没被访问过的点了
这样复杂度降到 \(O(nm\ logm)\)
发现 \(m\) 很小,每次我想要匹配一个点,如果这个点匹配不上的话,图是不变的,原来点的 \(vis\) 是什么样还是什么样,那我就不用清空 \(vis\),每次只有匹配上我才清空 \(vis\),如果匹配上了 \(m\) 个点就再也不用匹配了,所以复杂度是 \(O(nlogm + m^2logm)\)
HDU 6350
考虑仙人掌中的一个环,如果要割这个环上的边的话,不管割哪边,一定会割一个权值最小的边,我们把最小边断掉,再把它的权值加到环上其他边中,这样就把仙人掌转化成树了
把所有边的权值从大到小排序,每次拿出来的边是它连接的两个连通块的最大流,按位加进去就行了
这里有一个存储仙人掌上环和环边的小技巧,所以放下代码
\(code\)
#include <bits/stdc++.h>
using namespace std;
#define rg register
#define gc getchar
//char ss[1 << 17], *A = ss, *B = ss;
//inline char gc(){ if(A == B){ B = (A = ss) + fread(ss, 1, 1 << 17, stdin); if(A == B) return EOF; } return *A++; }
#define rep(i, a, b) for(int i = a; i <= b; ++i)
const int N = 1e5 + 5, M = N << 2;
inline int read(){
rg char ch = gc();
rg int x = 0, f = 0;
while(!isdigit(ch)) f |= (ch == '-'), ch = gc();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = gc();
return f ? -x : x;
}
struct edge{
int x, y, id, w;
edge(){}
edge(int x, int y, int id, int w): x(x), y(y), id(id), w(w) {}
inline bool operator < (const edge &rhs) const { return w > rhs.w; }
}e[M], to[M];
int cnt;
vector<edge> g[N];
int tot = 1, n, m;
int dep[N], fa[N];
int pos[N], f[N], sum[N][31][2];
unsigned long long ans;
int find(int x){
return f[x] ? f[x] = find(f[x]) : x;
}
bool inh[M];
inline void prepro(){
for(rg int i = 1; i <= n; ++i){
vector<edge>().swap(g[i]);
dep[i] = f[i] = pos[i] = fa[i] = 0;
}
rep(i, 2, tot) inh[i] = false;
cnt = ans = 0;
tot = 1;
}
void add(int x, int y, int z){
++tot; e[tot] = edge(x, y, tot, z);
g[x].push_back(e[tot]);
}
int s[N], stop, link[N];
inline void merge(int x, int y, int val){
x = find(x), y = find(y);
for(int i = 0; i < 31; ++i){
ans += (1ull << i) * (1ull * sum[x][i][0] * sum[y][i][1 ^ (val >> i & 1)]);
ans += (1ull << i) * (1ull * sum[x][i][1] * sum[y][i][(val >> i & 1)]);
if(pos[x] < pos[y]) sum[y][i][0] += sum[x][i][0], sum[y][i][1] += sum[x][i][1];
else sum[x][i][0] += sum[y][i][0], sum[x][i][1] += sum[y][i][1];
}
if(pos[x] < pos[y]) f[x] = y;
else f[y] = x;
if(pos[x] == pos[y]) ++pos[x];
}
void get(int x, int y, int id){
if(dep[x] < dep[y]) return;
s[stop = 1] = id;
while(x ^ y) s[++stop] = link[x], x = fa[x];
int st, res = 0x3f3f3f3f;
rep(i ,1, stop){ if(e[s[i]].w < res) res = e[s[i]].w, st = i; }
rep(i, 1, stop){
inh[s[i]] = inh[s[i] ^ 1] = true;
if(st ^ i){
e[s[i]].w += res;
to[++cnt] = e[s[i]];
}
}
}
void tarjan(int x){
for(vector<edge>:: iterator it = g[x].begin(); it != g[x].end(); ++it){
int y = it -> y;
if(y == fa[x]) continue;
if(!dep[y]){
fa[y] = x;
link[y] = it -> id;
dep[y] = dep[x] + 1;
tarjan(y);
}else get(x, y, it -> id);
}
}
inline void solve(){
dep[1] = 1;
tarjan(1);
for(int i = 2; i <= tot; i += 2) if(!inh[i]) to[++cnt] = e[i];
sort(to + 1, to + 1 + cnt);
rep(i, 1, cnt){
merge(to[i].x, to[i].y, to[i].w);
}
printf("%llu\n", ans);
}
inline void Main(){
prepro();
n = read(), m = read();
rep(i, 1, n) rep(j, 0, 30){
int ns = i >> j & 1;
sum[i][j][ns] = 1;
sum[i][j][!ns] = 0;
}
rep(i, 1, m){
int x = read(), y = read(), z = read();
add(x, y, z); add(y, x, z);
}
solve();
}
signed main(){
// freopen("input.in", "r", stdin);
int T = read();
while(T--) Main();
gc(), gc();
}
/*
1
3 3
1 2 4966
2 3 16318
1 3 29988
*/
AGC 031 E
有 \(N\) 个珠宝,第 \(i\) 个位于 \((x_i, y_i)\),价值为 \(v_i\)。你可以选择一些珠宝,有如下 \(4\) 种限制
- \(x \le a_i\) 的珠宝不超过 \(b_i\) 个
- \(x \ge a_i\) 的珠宝不超过 \(b_i\) 个
- \(y \le a_i\) 的珠宝不超过 \(b_i\) 个
- \(y \ge a_i\) 的珠宝不超过 \(b_i\) 个
\(N \le 80, x_i, a_i, b_i \le 100\)
喵喵题
发现一个珠宝对应好多种限制,如果用记录点看限制的常规思路太慢了
给点增加限制,把限制变成对点的约束
首先考虑一个维度,比如说 \(x\) 维
不妨设所有选出来的珠宝的横坐标递增 \(p_i \ge p_{i - 1}\)
那对于限制来说,(比如第一种限制)就变成了第 \(b_i + 1\) 个珠宝的位置必须 \(> a_i\) 第二种限制就是倒数第 \(b_i - 1\) 个点的位置 \(< a_i\)
这样可以转换为第 \(i\) 个珠宝只能在区间 \([L_i, R_i]\) 中选且 \(p_i \le p_{i + 1}\)
因为 \(p_i \le p_{i + 1}\) 所以我们要对 \(L_i\) 做一个前缀 \(max\),对 \(R_i\) 做一个后缀 \(min\)
然后发现如果 \(p_i > p_{i + 1}\) 那我交换两个珠宝,它还是满足限制的,因为 \(L_i \le L_{i + 1}, R_i \le R_{i + 1}\)
然后这就变成一个匹配模型了,排序后用 set 搞的匈牙利可以做到 \(O(n ^2 \ logn)\)
考虑加入纵坐标的限制
一个点有横纵坐标,要让它俩都满足限制,那可以分别把一个物品看成两个,然后把他们联系起来
每个点 \(x\) 向 \(y\) 连边,权值是价值,\(x\) 可以匹配一些区间,\(y\) 可以匹配一些区间,枚举一共选多少个珠宝,跑个费用流就行了
404_NotFound
给一个多项式 \(P\),\(P\),的每一项都是 \(x_i, y_i\) 或者 \(x_iy_j(1\le i , j \le n)\)。判断下式是否满足:
- 定理:当函数连续时,先对 \(x_1\) 求偏导,再对 \(y_1\) 求偏导 与 先对 \(x_1\) 求偏导,再对 \(y_1\) 求偏导得到的函数相同
我们发现只有当 \(x_1,y_1,...,x_n, y_n\) 恰好出现一次时,求完偏导是 \(1\),否则是 \(0\)
于是问题变成了,是否可以从 \(p\) 中选 \(k\) 个变量,形如 \(x_i, y_i, x_iy_j\),使得 \(x_1, y_1,...,x_n, y_n\) 恰好出现一次
这个东西看着就像一个匹配模型,事实也是如次
考虑网络流把 \(x_i\) 放到左边,把 \(y_i\) 放到右边,拆点 \(x'_i\) 向 \(x_i\) 连上界为 \(1\),下界为 \(1\) 的边, \(y'_i\) 向 \(y_i\) 连上界为 \(1\),下界为 \(1\) 的边
如果可以选 \(x_i\) 就从 \(x_i\) 向 \(T\) 连上下界为 \(1, 0\) 的边,如果可以选 \(y_i\) 就从 \(S\) 向 \(y'_i\) 连上下界为 \(1, 0\) 的边,如果是 \(x_iy_j\) 就从 \(x_i\) 向 \(y'_j\) 连上下界为 \(1, 0\) 的边
最后从 \(T\) 向 \(S\) 连上下界为 \(k, k\) 的边,看看是否可以跑一个可行流就行了