图论例题

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{2pi} n, frac{2pi * 2} n...)
在逐渐旋转的过程中,每次会有一个点进入或走出一段圆弧,对应到网络流上,就是加入 (or) 删除一条边,加入直接加就行了,删除需要退一下流,这样做以后只需要再跑一次 (Dinic) 即可
时间复杂度 (O(n^2sqrt 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) 条边的无向图,求

[sum_{i = 1} ^ n sum_{j = 1} ^ {i - 1} min{3, maxflow(i, j)} ]

(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
", 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
", 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(1le i , j le n))。判断下式是否满足:

[frac {partial} {partial x_1} frac {partial} {partial y_1} frac partial {partial x_2} frac partial {partial y_2} ... frac partial {partial x_n} frac partial {partial y_n} P ^ k |_{x_1, y_1, x_2, y_2, ..., x_n, y_n = 0} ]

  • 定理:当函数连续时,先对 (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) 的边,看看是否可以跑一个可行流就行了

原文地址:https://www.cnblogs.com/XiaoVsun/p/13054073.html