图论例题

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\) 条边的无向图,求

\[\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\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)\)。判断下式是否满足:

\[\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\) 的边,看看是否可以跑一个可行流就行了

posted @ 2020-06-06 11:40  __int256  阅读(121)  评论(0编辑  收藏  举报