图论复习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';
    }    
}
posted @ 2025-09-04 21:01  Zheng_iii  阅读(16)  评论(0)    收藏  举报