图论复习1
9.4 图论笔记
同余最短路
概述
同余最短路是一类用于优化完全背包问题的图论建模方法,可以发现,上面的例题都可以使用完全背包来进行解决,但是在总体积过大的情况下,这类问题不好被解决,因此我们可以引入同余最短路这一概念。
我们先从所有的 \(a_i\) 中取一个值,并对它的剩余系进行图论建模。(下方示例中规定 \(x\) 为该值)
rep(i,1,n-1)
{
int y; fin >> y;
rep(i,0,x-1)
E[i].pb({(i+y)%x,y});
}
这里的有向边表示从状态 \(i\) 出发,我们可以通过 \(y\) 这条边达到 \((i+y) \bmod x\) 这个状态,权值为 \(y\) 。
然后我们便可以通过最短路算法知道从点 \(0\) 出发到每个点的最短路径,由于对于每一个从起点可达的状态,它每次 \(v \leftarrow v+x\) 都可以抵达一个新的方案,由于是按照同余来进行分组,所以我们不需要去重。
这有效的利用了同余分组的性质来对原问题进行了一个优化。
il void solve()
{
int n,l,r,x; fin >> n >> l >> r >> x; l--;
rep(i,1,x-1) dis[i] = inf;
rep(i,1,n-1)
{
int y; fin >> y;
rep(i,0,x-1)
E[i].pb({(i+y)%x,y});
}
dij(0);
rep(i,0,x-1)
{
if(r >= dis[i]) ans += (r-dis[i])/x+1;
if(l >= dis[i]) ans -= (l-dis[i])/x+1;
}
fout << ans;
}
例题
https://www.luogu.com.cn/problem/P3403
题意:从位置 \(0\) 开始,每次可移动 \(x,y,z\) 层,查询在 \(h-1\) 层以内,有多少楼层可以达到。
https://www.luogu.com.cn/problem/P2371
题意:
给定, \(n, a_{1\dots n}, l, r\) 求有多少 \(b\in[l,r]\) 满足 \(\sum_{i=1}^n a_ix_i=b\) 存在非负整数解。
参考代码
例题1
const int N = 2e5+10;
int h,dis[N],ans,x,y,z,vis[N];
std::vector<pii> E[N];
std::priority_queue<pii> q;
il void solve()
{
fin >> h >> x >> y >> z; h--;
rep(i,0,x-1)
{
E[i].pb({(i+y)%x,y});
E[i].pb({(i+z)%x,z});
dis[i] = inf;
}
dis[0] = 0; q.push({0,0});
while(!q.empty())
{
int u = q.top().se; q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(auto [v,w] : E[u])
if(dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push({-dis[v],v});
}
}
rep(i,0,x-1)
if(h >= dis[i])
ans += (h-dis[i])/x + 1;
fout << ans ;
}
例题2
const int N = 5e5+10;
std::priority_queue<pii> q;
std::vector<pii> E[N];
int ans,dis[N],vis[N];
il void solve()
{
int n,l,r,x; fin >> n >> l >> r >> x; l--;
rep(i,1,x-1) dis[i] = inf;
rep(i,1,n-1)
{
int y; fin >> y;
rep(i,0,x-1)
E[i].pb({(i+y)%x,y});
}
q.push({0,0});
while(!q.empty())
{
int u = q.top().se; q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(auto [v,w] : E[u])
if(dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push({-dis[v],v});
}
}
rep(i,0,x-1)
{
if(r >= dis[i]) ans += (r-dis[i])/x+1;
if(l >= dis[i]) ans -= (l-dis[i])/x+1;
}
fout << ans;
}
最大流
概述
令 \(G = (V,E)\) 是一个有源汇点的网络,我们希望在 \(G\) 上指定一个合适的流 \(f\) ,以最大化整个网络的流量 \(|f|\) ,这一问题被称作最大流问题。
Dinic 算法
我们在增广之前先对原图使用 BFS 分层,根据当前节点与源点的距离将节点分成若干层,令经过 \(u\) 的流量只能流向下一层的节点 \(v\)。
当前弧优化
由于在 DFS 的过程中,当一个节点同时具有大量的邻接边,我们需要对所有的边都进行遍历,这个过程的最坏时间复杂度为 \(O(|E|^2)\) 。为了避免这一个缺陷,我们对于每个节点 \(u\) 维护出第一条还有必要尝试的出边,避免再次访问已经增广到极限的边。
多路增广
多路增广是 Dinic 中的一个常数优化,通过 DFS 的性质来避免重复的访问。
参考代码
const int N = 200100;
#define inf 0x3f3f3f3f
struct E {int to, w,nxt;} E[N<<1];
int tot = 1, H[N];
inline void Add(int u, int v, int c)
{
E[++tot] = {v,c,H[u]};
H[u] = tot;
}
inline void add(int u,int v,int c){Add(u,v,c),Add(v,u,0);}
int cur[N],d[N],vis[N],S,T;
int n,m,k;
bool bfs()
{
memset(d,-1,sizeof(d));
std::queue<int> q;
cur[S] = H[S];
q.push(S);d[S]=0;
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = H[u];~i;i = E[i].nxt)
{
int v = E[i].to ,w = E[i].w;
if(~d[v] || !w) continue;
d[v] = d[u]+1;
cur[v] = H[v];
if(v == T) return 1;
q.push(v);
}
}
return 0;
}
int dfs(int s,int lim)
{
if(s == T) return lim;
int flow = 0;
for(int &i = cur[s];~i && flow < lim; i = E[i].nxt)
{
int v = E[i].to;
if(d[v] != d[s]+1||!E[i].w) continue;
int t = dfs(v,std::min(E[i].w,lim-flow));
if(!t) d[v]=-1;
E[i].w-=t; E[i^1].w+=t;
flow+=t;
}
return flow;
}
int dinic()
{
int res = 0;
while (bfs()) res += dfs(S, inf);
return res;
}
最小割
概述
割:将原图分割成两个部分 \(S,T\) ,\(s \in S , t \in T\)。
割的容量,\(c(S,T) = \sum_{u \in S,v \in T}c(u,v)\)。
最小割:求得一个割 \((S,T)\) 使得割的容量最小。
根据最大流最小割定理,最小割的容量 = 最大流的流量。
例题
https://www.luogu.com.cn/problem/P1361
通过二选一这一设定,我们可以联想到最小割模型。
- 由源点S向它连一条边权为其种在 A 中的价值的边,表示将其种在 A 中。
- 由它向汇点T连一条边权为其种在 B 中的价值的边,表示将其种在 B 中。
- 对每一种组合进行建图,然后连两个权值无限大的边到两个点。
最优化答案即为总收益减去最小割。
费用流
在最大流的基础上多了一个叫费用的值。
在最大化流量的同时最小化每条边的费用与流量的乘积和。
我们只需要把最大流的 BFS 改成 SPFA 就可以了。
每次增广的时候,流量 \(+m\),那么费用增加 \(m×dis[t]\) 。
参考代码
const int N = 2e5 + 10;
const int INF = 1e18;
struct Ed {
int to, cap, w, nxt;
} E[N << 1];
int tot = 1, H[N];
int dis[N], vis[N], cur[N];
int n, m, S, T;
int maxFlow, minCost;
inline void Add(int u, int v, int cap, int w)
{
E[++tot] = {v, cap, w, H[u]};
H[u] = tot;
}
inline void addEdge(int u, int v, int cap, int w)
{
Add(u, v, cap, w);
Add(v, u, 0, -w);
}
bool spfa()
{
std::fill(dis, dis + N, INF);
std::queue<int> q;
q.push(S);
dis[S] = 0;
vis[S] = 1;
while (!q.empty())
{
int u = q.front();
q.pop();
vis[u] = 0;
for (int i = H[u]; ~i; i = E[i].nxt)
{
int v = E[i].to;
if (E[i].cap > 0 && dis[v] > dis[u] + E[i].w)
{
dis[v] = dis[u] + E[i].w;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
return dis[T] != INF;
}
int dfs(int u, int flow)
{
if (u == T) return flow;
vis[u] = 1;
int res = 0;
for (int &i = cur[u]; ~i; i = E[i].nxt)
{
int v = E[i].to;
if (E[i].cap > 0 && !vis[v] && dis[v] == dis[u] + E[i].w)
{
int di = dfs(v, std::min(flow - res, E[i].cap));
if (di > 0)
{
E[i].cap -= di;
E[i ^ 1].cap += di;
res += di;
minCost += di * E[i].w;
if (res == flow) break;
}
}
}
vis[u] = 0;
return res;
}
void dinic()
{
maxFlow = 0;
minCost = 0;
while (spfa())
{
memcpy(cur, H, sizeof(H));
while (int di = dfs(S, INF))
maxFlow += di;
}
}
void solve()
{
memset(H, -1, sizeof(H));
int x;
fin >> n >> m >> S >> T;
for (int i = 1; i <= m; i++)
{
int u, v, cap, cost;
fin >> u >> v >> cap >> cost;
addEdge(u, v, cap, cost);
}
dinic();
fout << maxFlow << ' ' << minCost << '\n';
}
9.5 图论笔记
线段树优化建图
例题
https://codeforces.com/problemset/problem/786/B
题意:
有 \(n\) 个点,\(q\) 个询问,每次询问给出一个操作。
操作 1:\(1\ u\ v\ w\),从 \(u\) 向 \(v\) 连一条权值为w的有向边。
操作 2:\(2\ u\ l\ r\ w\),从u向区间 \([l,r]\) 的所有点连一条权值为w的有向边。
操作 3:\(3\ u\ l\ r\ w\),从区间[l,r]的所有点向 \(u\) 连一条权值为w的有向边。
建完边跑一遍最短路。
概述
建两棵线段树,一棵出树,一棵入树,分别维护出边和入边,每次区间连边就从叶子节点连向出\入树的区间节点。
参考代码
const int N = 2e6+10;
std::vector<pii> E[N];
int lc[N],rc[N],tot,ncnt;
int n,m,s,rt1,rt2;
void buildout(int &u,int l,int r)
{
if(l == r) {u = l; return ;}
u = ++ncnt;
int mid = l+r >> 1;
buildout(lc[u],l,mid);
buildout(rc[u],mid+1,r);
E[u].pb({lc[u],0});
E[u].pb({rc[u],0});
}
void buildin(int &u,int l,int r)
{
if(l == r) {u = l; return ;}
u = ++ncnt;
int mid = l+r >> 1;
buildin(lc[u],l,mid);
buildin(rc[u],mid+1,r);
E[lc[u]].pb({u,0});
E[rc[u]].pb({u,0});
}
int L,R;
void update(int u,int l,int r,int x,int w,int op)
{
if(L <= l && r <= R)
{
op == 2
? E[x].pb({u,w})
: E[u].pb({x,w});
return ;
}
int mid = l+r >> 1;
if(L <= mid) update(lc[u],l,mid,x,w,op);
if(R > mid) update(rc[u],mid+1,r,x,w,op);
}
int dis[N],vis[N];
std::priority_queue<pii> q;
il void dij(int s)
{
mem(dis,0x3f),mem(vis,0);
dis[s] = 0;
q.push({0,s});
while(!q.empty())
{
int u = q.top().se; q.pop();
if(vis[u]) continue; vis[u] = 1;
for(auto [v,w] : E[u])
if(!vis[v] && dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push({-dis[v],v});
}
}
}
il void solve()
{
fin >> n >> m >> s;
ncnt = n;
buildout(rt1,1,n), buildin(rt2,1,n);
while(m--)
{
int c,u,v,w,l,r;
fin >> c;
if(c == 1)
{
fin >> u >> v >> w;
E[u].pb({v,w});
}
else
{
fin >> u >> L >> R >> w;
update(c==2?rt1:rt2,1,n,u,w,c);
}
}
dij(s);
rep(i,1,n)
if(dis[i] < inf) fout << dis[i] << ' ';
else fout << -1 << ' ';
}
Kruskal 重构树
概述
Kruskal 重构树就是将链接的边权抽象为点,每次合并连通块时创建新节点,并将点权设置为两点之间的边权,这样构造出来的树形结构有以下性质。
原图中两个点间所有路径上的边最大权值的最小值 = 最小生成树上两点简单路径的边最大权值 = Kruskal 重构树上两点 LCA 的点权。
例题
https://www.luogu.com.cn/problem/P2245
给定一张图,求两点之间的最小瓶颈路。
参考代码
const int N = 3e5+10;
int n,m,fa[N],tot,a[N],siz[N],son[N],dep[N],q,Fa[N];
vi E[N];
struct Ed{int u,v,w;}e[N];
int find(int x)
{
if(fa[x] == x) return fa[x];
return fa[x] = find(fa[x]);
}
il void merge(int x,int y)
{
x = find(x) , y = find(y);
fa[y] = x;
}
il void add(int u,int v)
{
E[u].eb(v);
E[v].eb(u);
}
int dfn[N],top[N];
void dfs1(int u)
{
siz[u] = 1;
for(int v : E[u])
{
if(dep[v]) continue;
Fa[v] = u;
dep[v] = dep[u] + 1;
dfs1(v);
siz[u] += siz[v];
if(siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u,int tp)
{
top[u] = tp;
if(!son[u]) return ;
dfs2(son[u],tp);
for(int v : E[u])
{
if(v == Fa[u] || v == son[u]) continue;
dfs2(v,v);
}
}
il int lca(int x,int y)
{
while(top[x] != top[y])
{
if(dep[top[x]] < dep[top[y]]) std::swap(x,y);
x = Fa[top[x]];
}
if(dep[x] < dep[y]) return x;
return y;
}
il void solve()
{
fin >> n >> m;
rep(i,1,n*2) fa[i] = i;
tot = n;
rep(i,1,m)
{
int u,v,w; fin >> u >> v >> w;
e[i] = {u,v,w};
}
std::sort(e+1,e+1+m,[](Ed a,Ed b){
return a.w < b.w;
});
rep(i,1,m)
{
int v = find(e[i].v);
int u = find(e[i].u);
if(find(v) != find(u))
{
fa[u] = fa[v] = ++tot;
add(u,tot); add(v,tot);
a[tot] = e[i].w;
}
}
per(i,tot,1) if(!dep[i]) dep[i] = 1,dfs1(i),dfs2(i,i);
fin >> q;
while(q--)
{
int x,y; fin >> x >> y;
if(find(x) != find(y))
fout << "impossible\n";
else
fout << a[lca(x,y)] << '\n';
}
}

浙公网安备 33010602011771号