ACM图论模板

最短路

Dijkstra不能使用在含负权的图中
朴素版dijstra:时间复杂度 O ( n 2 ) O(n^2) O(n2)

#include<bits/stdc++.h>
#define ll long long
#define mem(a) memset(a,0,sizeof(a))
using namespace std;

int n,m,g[600][600];
int d[600];
bool vis[600];
int dijstra(){
	memset(d,0x3f,sizeof d);	
	d[1]=0;
	for(int j=1;j<=n;j++){
		int t=-1;
		for(int i=1;i<=n;i++){
			if(!vis[i] &&( t==-1||d[i]<d[t]))
				t=i;
		}
		vis[t]=true;
		for(int i=1;i<=n;i++){
			d[i]=min(d[i],d[t]+g[t][i]);
		}
	}
	if(d[n]==0x3f3f3f3f) return -1;
	return d[n];
}

int main(){
	int a,b,v;
	cin>>n>>m;
	memset(g,0x3f,sizeof g);
	for(int i=0;i<m;i++){
		cin>>a>>b>>v;
		g[a][b]=min(g[a][b],v);
	}
	cout<<dijstra()<<endl; 
	return 0;
}

堆优化版dijstra:时间复杂度 O ( m l o g ( n ) ) O(mlog(n)) O(mlog(n))

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N = 2e5+5;
int head[N],e[N],ne[N],w[N];
int d[N],idx,n,m;
bool vis[N];
void add(int a,int b,int v){
    e[idx]=b;w[idx]=v;ne[idx]=head[a];head[a]=idx++;
}

int dijkstra(){
    memset(d,0x3f,sizeof d);
    d[1]=0;
    priority_queue<PII,vector<PII>,greater<PII> >heap;
    heap.push({0,1});
    while(heap.size()){
        int t=heap.top().second;
        heap.pop();
        if(vis[t]) continue ;
        vis[t]=true;
        for(int i=head[t];i!=-1;i=ne[i]){
            int j=e[i];
            if(d[j]>d[t]+w[i]){
                d[j]=d[t]+w[i];
                heap.push({d[j],j});
            }
        }
    }
    if(d[n]==0x3f3f3f3f) return -1;
    return d[n];
}

int main(){
    int a,b,v;
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof head);
    while(m--){
        scanf("%d%d%d",&a,&b,&v);
        add(a,b,v);
    }
    cout<<dijkstra()<<endl;
    return 0;
}

Bellman - ford
可以求含负权图的单源最短路径
解决有边数限制的最短路问题

#include<bits/stdc++.h>
using namespace std;

const int N = 1e4+5;
int n,m,k,d[N];
struct node{
    int a,b,v;
}edge[N];
int pre[N];
int bellman_ford(){
    memset(d,0x3f,sizeof d);
    d[1]=0;
    for(int i=0;i<k;i++){
        memcpy(pre,d,sizeof d);
        for(int j=0;j<m;j++){
            int a=edge[j].a,b=edge[j].b,v=edge[j].v;
            d[b]=min(d[b],pre[a]+v);
        }
    }
}

int main(){
    //n个点,m条边,求经过k条边的最短路 
    cin>>n>>m>>k;
    for(int i=0;i<m;i++){//读入边
        cin>>edge[i].a>>edge[i].b>>edge[i].v;
    }
    bellman_ford();
    if(d[n]>0x3f3f3f3f/2) cout<<"impossible"<<endl;
    else cout<<d[n]<<endl;
    return 0;
}

spfa
图中有负权回路用spfa会死循环

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+5,M = 2e5+5;
int e[M],ne[M],w[M],head[N],idx,d[N],q[M];
bool vis[N];
void add(int a,int b,int c){
    e[idx] = b;w[idx] = c;ne[idx] = head[a];head[a] = idx++;
}
/*
bellman : 每一次遍历一下所有的边,把能更新的点都更新了,重复这个操作n-1次。
            一定能求出最短n到1的最短距离
spfa :把当前能更新的点先更新一下,一直更新到所有点都不能更新为止。
*/
int spfa(){
    memset(d,0x3f,sizeof d);
    int hh = 0, tt = 0;
    q[0] = 1;d[1] = 0;
    vis[1] = true;
    while(hh <= tt){
        int t = q[hh++];
        vis[t] = false;
        for(int i = head[t]; ~i; i = ne[i]){
            int j = e[i];
            if(d[j]>d[t]+w[i]){
                d[j] = d[t] + w[i];
                if(!vis[j]){
                    q[++tt] = j;
                    vis[j] = true;
                }
            }
        }
    }
}

int main(){
    int n,m;
    cin>>n>>m;
    memset(head,-1,sizeof head);
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    spfa();
    if(d[n] == 0x3f3f3f3f) cout<<"impossible"<<endl;
    else cout<<d[n]<<endl;
    return 0;
}

Floyd

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;
int d[N][N];

void floyd()
{
   for (int k = 1; k <= n; k ++ )
       for (int i = 1; i <= n; i ++ )
           for (int j = 1; j <= n; j ++ )
               d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main()
{
   scanf("%d%d%d", &n, &m, &Q);

   for (int i = 1; i <= n; i ++ )
       for (int j = 1; j <= n; j ++ )
           if (i == j) d[i][j] = 0;
           else d[i][j] = INF;

   while (m -- )
   {
       int a, b, c;
       scanf("%d%d%d", &a, &b, &c);
       d[a][b] = min(d[a][b], c);
   }

   floyd();

   while (Q -- )
   {
       int a, b;
       scanf("%d%d", &a, &b);

       int t = d[a][b];
       if (t > INF / 2) puts("impossible");
       else printf("%d\n", t);
   }

   return 0;
}

最小生成树

prim算法

#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 505;
int n,m;
int g[N][N],d[N],res;
bool vis[N];
int prim(){
    memset(d,0x3f,sizeof d);
    d[1]=0;
    for(int i=0;i<n;i++){
        int t=-1;
        for(int j=1;j<=n;j++)
            if(!vis[j] && (t==-1 || d[j]<d[t]))
                t=j;
        
        vis[t]=true;
        res+=d[t];
        //cout<<res<<endl;
        if(d[t]==INF) return INF;
        for(int j=1;j<=n;j++){
            d[j]=min(d[j],g[t][j]);
        }
        
    }
    return res;
}

int main(){
    memset(g,0x3f,sizeof g);
    cin>>n>>m;
    int a,b,v;
    while(m--){
        cin>>a>>b>>v;
        g[a][b]=g[b][a]=min(g[a][b],v);
    }
    int t=prim();
    if(t==INF) cout<<"impossible"<<endl;
    else cout<<res<<endl;
    return 0;
}

kruskal算法

#include<cstdio>
#include<algorithm>
#define ll long long
#define mem(a) memset(a,0,sizeof(a))
using namespace std;

struct node{
	int x,y,w;
	bool operator < (const node &a) const {
		return w<a.w;
	}
}a[105];
int n,m,f[105];
int find(int x){
	return x==f[x]?x:f[x]=find(f[x]);
}
bool judge(int i){
	int fx=find(a[i].x);
	int fy=find(a[i].y);
	if(fx==fy)return false ;
	f[fx]=fy;
	return true;
}
int main(){
	int x,y,w,k;
	while(~scanf("%d",&n) && n!=0){
		m=0;
		for(int i=1;i<=n;i++) f[i]=i;
		n=n*(n-1)/2;
		for(int i=0;i<n;i++){
			scanf("%d%d%d%d",&x,&y,&w,&k);
			if(k) a[m++]={x,y,0};
			else a[m++]={x,y,w};
		}
		sort(a,a+m);
		int ans=0;
		for(int i=0;i<m;i++){
			if(judge(i)){
				ans+=a[i].w ;
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

差分约束

Acwing362. 区间
给定 n 个区间 [ a i , b i ] 和 n 个整数 c i 。你需要构造一个整数集合 Z , 使得 ∀ i ∈ [ 1 , n ] , Z 中满足 a i ≤ x ≤ b i 的整数 x 不少于 c i 个。 求这样的整数集合 Z 最少包含多少个数。 我们定义 S [ i ] 为: 1 到 i 中被选出数的个数。我们最终要求解的就是 S 50001 的最小值,因此需要使用最长路径 . 给定 n 个区间 [a_i,b_i] 和 n 个整数 c_i。你需要构造一个整数集合 Z,\\使得 ∀i∈[1,n],Z 中满足 a_i≤x≤b_i 的整数 x 不少于 c_i 个。\\求这样的整数集合 Z 最少包含多少个数。 \\我们定义S[i]为:1到 i中被选出数的个数。我们最终要求解的就是S_{50001}的最小值,因此需要使用最长路径. 给定n个区间[ai,bi]n个整数ci。你需要构造一个整数集合Z使得i[1,n]Z中满足aixbi的整数x不少于ci个。求这样的整数集合Z最少包含多少个数。我们定义S[i]为:1i中被选出数的个数。我们最终要求解的就是S50001的最小值,因此需要使用最长路径.

#include<bits/stdc++.h>
using namespace std;

const int N = 5e4+5, M = 2e5;
int e[M],ne[M],head[N],w[M],idx;
int q[N],d[N];
bool vis[N];

void add(int a,int b,int c){
    e[idx]=b;w[idx]=c;ne[idx]=head[a];head[a]=idx++;
}

int spfa(){
    memset(d,-0x3f,sizeof d);
    int hh=0,tt=1;
    d[0]=0;q[0]=0;vis[0]=true;
    while(hh!=tt){
        int t = q[hh++];
        vis[t]=false;
        if(hh==N) hh=0;
        for(int i=head[t];~i;i=ne[i]){
            int j=e[i];
            if(d[j]<d[t]+w[i]){
                d[j]=d[t]+w[i];
                if(!vis[j]){
                    q[tt++]=j;
                    vis[j]=true;
                    if(tt==N) tt=0;
                }
            }
        }
    }
}

/*
求si的最小值,即求最长路
题目满足:
Si >= Si-1
Si - Si-1 <= 1
Sb - Sa >= c
    <=>
Sb >= Sa+c
    <=>
建一条从a连向b的边,在求完最长路后,满足上面条件
*/
int main(){
    int n;
    memset(head,-1,sizeof head);
    for(int i=1;i<=50001;i++){
        add(i-1,i,0);
        add(i,i-1,-1);
    }
    cin>>n;
    while(n--){
        int a,b,c;
        cin>>a>>b>>c;
        a++,b++;
        add(a-1,b,c);
    }
    spfa();
    cout<<d[50001]<<endl;
    return 0;
}

最近公共祖先

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 4e4 + 10, M = 2 * N, lgN = 15;

int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], q[N], fa[N][lgN + 1];

void add(int u, int v)
{
    e[idx] = v, ne[idx] = h[u], h[u] = idx ++;
}

void bfs(int root)
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0; //depth[NULL] = 0, 方便步骤1 防止跳出根的判断
    depth[root] = 1;
    int hh = 0, tt = 0;
    q[tt ++] = root;

    while( hh != tt )
    {
        int u = q[hh ++];

        for( int i = h[u]; ~i; i = ne[i] )
        {
            int v = e[i];
            if( depth[v] > depth[u] + 1 ) //未被遍历
            {
                depth[v] = depth[u] + 1;
                q[tt ++] = v;

                fa[v][0] = u; //直接父节点
                for( int k = 1; k <= lgN; k ++ )
                {//递推 如果超出根 fa[v][k] = 0
                    fa[v][k] = fa[ fa[v][k - 1] ][k - 1];   
                }
            }
        }
    }
}

int lca(int a, int b)
{
    if( depth[a] < depth[b] )   swap(a, b); //保证depth[a] >= depth[b]

    for( int k = lgN; k >= 0; k -- )
    {//跳出根会跳过, 所以k可以从最大可能值开始判断
        int pa = fa[a][k]; //如果跳出 depth[NULL] = 0, 不会跳
        if( depth[pa] >= depth[b] )
        {
            a = pa; //倍增思想 能减去则减
        }
    }

    if( a == b )    return a; //其中一个点是两点的公共祖先

   // return 0; 直接返回NULL即可 因为此时两点都不是公共祖先

   for( int k = lgN; k >= 0; k -- )
   {
       int pa = fa[a][k], pb = fa[b][k];
       if( pa != pb )
       {//跳到离公共祖先最近的节点 因为是同一层 所以如果跳出则同时跳出 均为0 会被条件判断pass掉
           a = pa;
           b = pb;
       }
   }
   return fa[a][0]; //lca为两个节点的上一层
}

int main()
{
    cin >> n;
    int root;
    memset(h, -1, sizeof h);
    while( n -- )
    {
        int a, b;
        cin >> a >> b;
        if( b == -1 )   root = a;
        else    add(a, b), add(b, a);
    }

    bfs(root); //预处理

    cin >> m;
    while( m -- )
    {
        int a, b;
        cin >> a >> b;
        int p = lca(a, b);
        if( p == a )    puts("1");
        else if( p == b )   puts("2");
        else    puts("0");
    }

    return 0;
}

题目:求树上两点最短距离

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 10010, M = N * 2;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int p[N];
int res[M];
int st[N];
vector<PII> query[N];   // first存查询的另外一个点,second存查询编号

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        dist[j] = dist[u] + w[i];
        dfs(j, u);
    }
}

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void tarjan(int u)
{
    st[u] = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            tarjan(j);
            p[j] = u;
        }
    }

    for (auto item : query[u])
    {
        int y = item.first, id = item.second;
        if (st[y] == 2)
        {
            int anc = find(y);
            res[id] = dist[u] + dist[y] - dist[anc] * 2;
        }
    }

    st[u] = 2;
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
	// 存询问
    for (int i = 0; i < m; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        if (a != b)
        {
            query[a].push_back({b, i});
            query[b].push_back({a, i});
        }
    }

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    dfs(1, -1);
    tarjan(1);

    for (int i = 0; i < m; i ++ ) printf("%d\n", res[i]);

    return 0;
}

次小生成树

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010, M = 300010, INF = 0x3f3f3f3f;

int n, m;
struct Edge
{
    int a, b, w;
    bool used;
    bool operator< (const Edge &t) const
    {
        return w < t.w;
    }
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

LL kruskal()
{
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    sort(edge, edge + m);
    LL res = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
        if (a != b)
        {
            p[a] = b;
            res += w;
            edge[i].used = true;
        }
    }

    return res;
}

void build()
{
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ )
        if (edge[i].used)
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            add(a, b, w), add(b, a, w);
        }
}

void bfs()
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    q[0] = 1;
    int hh = 0, tt = 0;
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[ ++ tt] = j;
                fa[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -INF;
                for (int k = 1; k <= 16; k ++ )
                {
                    int anc = fa[j][k - 1];
                    fa[j][k] = fa[anc][k - 1];
                    int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
                    d1[j][k] = d2[j][k] = -INF;
                    for (int u = 0; u < 4; u ++ )
                    {
                        int d = distance[u];
                        if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
                        else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
                    }
                }
            }
        }
    }
}

int lca(int a, int b, int w)
{
    static int distance[N * 2];
    int cnt = 0;
    if (depth[a] < depth[b]) swap(a, b);
    for (int k = 16; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b])
        {
            distance[cnt ++ ] = d1[a][k];
            distance[cnt ++ ] = d2[a][k];
            a = fa[a][k];
        }
    if (a != b)
    {
        for (int k = 16; k >= 0; k -- )
            if (fa[a][k] != fa[b][k])
            {
                distance[cnt ++ ] = d1[a][k];
                distance[cnt ++ ] = d2[a][k];
                distance[cnt ++ ] = d1[b][k];
                distance[cnt ++ ] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }
        distance[cnt ++ ] = d1[a][0];
        distance[cnt ++ ] = d1[b][0];
    }

    int dist1 = -INF, dist2 = -INF;
    for (int i = 0; i < cnt; i ++ )
    {
        int d = distance[i];
        if (d > dist1) dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2) dist2 = d;
    }

    if (w > dist1) return w - dist1;
    if (w > dist2) return w - dist2;
    return INF;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edge[i] = {a, b, c};
    }

    LL sum = kruskal();
    build();
    bfs();

    LL res = 1e18;
    for (int i = 0; i < m; i ++ )
        if (!edge[i].used)
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            res = min(res, sum + lca(a, b, w));
        }
    printf("%lld\n", res);

    return 0;
}

有向图的强连通分量

tarjan算法可以用来求强连通分量(SCC)
作用: 将有向图(通过缩点,将所有连通分量缩成一点)——>有向无环图(DAG)满足拓扑序。
tarjan算法梳理的思路:
1.加时间戳;
2.放入栈中,做好标记;
3.遍历邻点:
1)如果没遍历过,tarjan一遍,用low[j]更新最小值low;
2) 如果在栈中,用dfn[j]更新最小值low
4.找到最高点:
1)scc个数++
2)do-while循环:从栈中取出每个元素;标志为出栈;对元素做好属于哪个scc;该scc中点的数量++
例题:Acwing1174. 受欢迎的牛
现在有 N 头牛,编号从 1 到 N,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。求出有多少头牛被除自己之外的所有牛认为是受欢迎的。
思路:
当出度为0的强连通分量只有一个,则该强连通分量中的所有点都被其他强连通分量的牛欢迎,且连通分量里的牛是相互欢迎的。所以答案为出度为0强连通分量里的节点数。
但假如存在两及以上个出度为0的强连通分量,则没有牛被所有牛欢迎。

#include<bits/stdc++.h>
using namespace std;

const int N = 1e4+5, M = 5e5+5;
int head[N],ne[M],e[M],idx;
int dfn[N],low[N],te;
//dfn[u]:表示遍历到u的时间戳
//low[u]:表示从u开始走,所能遍历到的最小时间戳
int stk[N],top;
bool in_stk[N];
int id[N],scc_cnt,sz[N];//每个强连通分量的节点个数
int dout[N];

void add(int a,int b){
    e[idx] = b, ne[idx] = head[a], head[a] = idx++;
}


void tarjan(int u){
    low[u] = dfn[u] = ++te;//u的时间戳
    stk[++top] = u; in_stk[u] = true;
    for(int i = head[u]; ~i; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){//j点为遍历过
            tarjan(j);
            low[u] = min(low[u], low[j]);
            //用儿子节点j能走的最小时间戳更新u能走到的最小时间戳
        }
        else if(in_stk[j]){
            //如果那个点还在栈中
            //说明出现了一个强连通分量SCC,而且当前这个强连通分量还没有被遍历完
            //则这个j,要么是当前点u的祖先,要么是u通过横叉边连到另一个强联通分量
            //无论如何,这个j的时间戳都必定小于当前u的时间戳
            //我们就用他的时间戳来更新一下u的low
            low[u] = min(low[u], dfn[j]);
        }
    }
    // 所以当遍历完u的所有能到的点后 发现u最高能到的点是自己
    // 1 则u为强连通分量中的最高点,则以u为起点往下把该强连通分量所有节点都找出来
    // 2 要么它就没有环,就是一个正常的往下的点
    if(low[u] == dfn[u]){
        ++scc_cnt;
        int y;
        do{
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            sz[scc_cnt] ++;
        }while(y != u);
    }
}

int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof head);
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    
    for(int i=1;i<=n;i++){
        if(!low[i]) tarjan(i);
    }
    //缩点,建立拓扑图(DAG)
    for(int i=1;i<=n;i++){
        for(int j=head[i];~j;j=ne[j]){
            int k = e[j];
            int a = id[i], b = id[k];
            if(a != b) dout[a]++;
        }
    }
    //和题目有关:如果出度为0的点大于1个,则这这些出度为0的连通块里的人不能相互喜欢,所以答案为0
    //如果出度为0的点只有一个,这该点连通分量的节点个数就是答案
    int zeros = 0, sum = 0;
    for(int i=1;i<=scc_cnt;i++){
        if(!dout[i]){
            zeros++;
            sum+=sz[i];
            if(zeros > 1){
                sum = 0;
                break;
            }
        }
    }
    printf("%d",sum);
    
    return 0;
}

用tarjan解决差分约束问题
例题
AcWing 368. 银河
做法:
1 不等式建图
2 超级源点连边(加绝对关系限制)
3 跑tarjan算法
4 建立拓扑图(缩点)
5 如果SCC内部有正边,返回无解
6 按照拓扑图跑一边求从0点到每个SCC的最长路
7 加总每个SCC的最长路 * SCC的size

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010, M = 600010;
//注意边要开的空间大小
int n, m;
int h[N], hs[N], e[M], ne[M], w[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, sz[N];
int dist[N];

void add(int h[], int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u, in_stk[u] = true;

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
    }

    if (dfn[u] == low[u])
    {
        ++ scc_cnt;
        int y;
        do {
            y = stk[top -- ];
            in_stk[y] = false;
            id[y] = scc_cnt;
            sz[scc_cnt] ++ ;
        } while (y != u);
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    memset(hs, -1, sizeof hs);

    for (int i = 1; i <= n; i ++ ) add(h, 0, i, 1);

    while (m -- )
    {
        int t, a, b;
        scanf("%d%d%d", &t, &a, &b);
        if (t == 1) add(h, b, a, 0), add(h, a, b, 0);
        else if (t == 2) add(h, a, b, 1);
        else if (t == 3) add(h, b, a, 0);
        else if (t == 4) add(h, b, a, 1);
        else add(h, a, b, 0);
    }

    tarjan(0);

    bool success = true;
    for (int i = 0; i <= n; i ++ )
    {
        for (int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if (a == b)
            {
                if (w[j] > 0)
                {
                    success = false;
                    break;
                }
            }
            else add(hs, a, b, w[j]);
        }
        if (!success) break;
    }

    if (!success) puts("-1");
    else
    {
        for (int i = scc_cnt; i; i -- )
        {
            for (int j = hs[i]; ~j; j = ne[j])
            {
                int k = e[j];
                dist[k] = max(dist[k], dist[i] + w[j]);
            }
        }

        LL res = 0;
        for (int i = 1; i <= scc_cnt; i ++ ) res += (LL)dist[i] * sz[i];

        printf("%lld\n", res);
    }

    return 0;
}

AcWing 367. 学校网络
题目大意:最少加多少有向边,能使图变为有向图的强连通分量?
结论: m a x { a , b } max\{a,b\} max{a,b}(缩点以后,入度为0的点的数量记为a,出度为0的点的数量记为b)

#include<bits/stdc++.h>
using namespace std;

const int N = 105, M = 1e4+5;
int head[N],e[M],ne[M],idx;
int dfn[N],low[N],te;
int stk[N],top;
bool in_stk[N];
int scc_cnt,id[N];
int din[N],dout[N];

void add(int a,int b){
    e[idx]=b,ne[idx]=head[a],head[a]=idx++;
}

void tarjan(int u){
    dfn[u]=low[u]=++te;
    stk[++top]=u,in_stk[u]=true;
    for(int i=head[u];~i;i=ne[i]){
        int j=e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u] = min(low[u],low[j]);
        }
        else if(in_stk[j]){
            low[u] = min(low[u], dfn[j]);
        }
    }
    if(low[u] == dfn[u]){
        ++scc_cnt;
        int y;
        do{
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
        }while(y!=u);
    }
}

int main(){
    int n;
    cin>>n;
    memset(head,-1,sizeof head);
    for(int i=1;i<=n;i++){
        int x;
        while(cin>>x,x){
            add(i,x);
        }
    }
    for(int i=1;i<=n;i++){
        if(!dfn[i]) tarjan(i);
    }
    
    for(int i=1;i<=n;i++){
        for(int j=head[i];~j;j=ne[j]){
            int k = e[j];
            int a = id[i], b = id[k];
            if(a!=b){
                din[b]++;
                dout[a]++;
            }
        }
    }
    int a = 0, b = 0;
    for(int i=1;i<=scc_cnt;i++){
        if(!din[i]) a++;
        if(!dout[i]) b++;
    }
    cout<<a<<endl;
    if(scc_cnt==1) cout<<0<<endl;
    else cout<<max(a,b)<<endl;
    return 0;
}

无向图的强连通分量

  • 割点:无向连通图中,某点和连接的点的边去掉后,图不再连通。
  • 桥:无向连通图中,某条边去掉后,图不再连通。
  • 点的双连通分量(e-dcc):极大的不包含桥的连通块。
  • 边的双连通分量(v-dcc):极大的不包含割点的连通块。

Acwing 395. 冗余路径
题意:给定一个连通的无向图让你进行加边操作,要求每一对点之间都至少有两条相互分离的路径,求最小的加边数。
思路:从一个点到另一个点不能有桥,所以我们要个图加边使得无向图没有桥(即变为变双连通分量)。
代码:

#include<bits/stdc++.h>
using namespace std;

/*
加边,使每一对草场之间都会至少有两条相互分离的路径,
<=>
图为边双连通分量

所以本题解法是:
用tarjan算法将图变为生成树,每个树的点代表一个边双连通分量,边为桥
结论:树度数为1的点为cnt,答案为(cnt+1)/2;
即最少加上(cnt+1)/2的边能使图变为边双连通分量
*/

const int N = 5005, M = 2e4+5;
int head[N],e[M],ne[M],idx;
int dfn[N],low[N],te;
int stk[N],dcc_cnt,top;
int is_bridge[N],id[N],d[N];

void add(int a,int b){
    e[idx]=b,ne[idx]=head[a],head[a]=idx++;
}
                                                                                                                            
void tarjan(int u,int from){//现在的点和到这个点的边
    dfn[u]=low[u]=++te;
    stk[++top] = u;
    
    for(int i=head[u];~i;i=ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j,i);
            low[u] = min(low[u], low[j]);
            if(dfn[u] < low[j]){
                is_bridge[i] = is_bridge[i^1] = true;
            }
        }
        //j遍历过,且i不是反向边,用j的时间戳更新u
        else if(i != (from ^ 1))//非反向边
            low[u] = min(low[u], dfn[j]);
    }
    
    if(dfn[u] == low[u]){
        ++dcc_cnt;
        int y;
        do{
            y = stk[top--];
            id[y] = dcc_cnt;
        }while(y!=u);
    }
}

int main(){
    memset(head,-1,sizeof head);
    int n,m;
    cin>>n>>m;
    while(m--){
        int a,b;
        cin>>a>>b;
        add(a,b);add(b,a);
    }
    tarjan(1,-1);
    
    for(int i=0;i<idx;i++){
        //如果i是桥,则其的出边的点所在的双连通分量里的度+1
        if(is_bridge[i]){
            d[id[e[i]]] ++;
        }
    }
    
    int cnt = 0;
    for(int i=1;i<=dcc_cnt;i++){
        if(d[i]==1) cnt++;
    }
    cout<<(cnt+1)/2<<endl;
    
    return 0;
}

HDU4738 Caocao’s Bridges
题意:给定n个兵营和m座桥,每座桥上有若干个敌人,要求炸掉最多一座桥使得n个兵营和m座桥连成的无向图被分为至少两个连通块。炸一座桥至少出动一名士兵并且出动的士兵个数不能小于桥上的敌人个数,桥上可能没有敌人。
思路:炸掉一座桥使得无向图变成2个以上的连通图。也就是求权值最小的割边(桥)。
代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 1e3+5, M = 2e6+10, INF = 0x3f3f3f3f;
int head[N],e[M],ne[M],w[M],idx;
int dfn[N],low[N],te;
bool is_bridge[M];
int ans;

void add(int a,int b,int c){
    e[idx]=b,w[idx]=c,ne[idx]=head[a],head[a]=idx++;
}

void tarjan(int u,int from){
    dfn[u] = low[u] = ++te;

    for(int i=head[u];~i;i=ne[i]){
        int v = e[i];
        if(!dfn[v]){
            tarjan(v,i);
            low[u] = min(low[u], low[v]);
            if(dfn[u] < low[v]){
                is_bridge[i] = is_bridge[i^1] = true;
            }
        }
        else if(i != (from ^ 1)){
            low[u] = min(low[u], dfn[v]);
        }
    }
}

int main(){
    int n,m;
    while(cin>>n>>m){
        if(n + m == 0) break;
        memset(dfn,0,sizeof dfn);
        memset(low,0,sizeof low);
        memset(head,-1,sizeof head);
        memset(is_bridge,false,sizeof is_bridge);
        te = 0; ans = INF; idx = 0;
        while(m--){
            int a,b,c;
            cin>>a>>b>>c;
            add(a,b,c);
            add(b,a,c);
        }
        int num = 0;
        for(int i=1;i<=n;i++){
            if(!dfn[i]){
                tarjan(i,-1);
                num++;
            }
        }
        for(int i=0;i<idx;i+=2){
            if(is_bridge[i]){
                ans = min(ans, w[i]);
            }
        }
        if(num >= 2) cout<<0<<endl;//有两个以上的连通图
        else if(ans == INF) cout<<-1<<endl; //没有桥,图为一个边双连通分量
        else if(ans == 0) cout<<1<<endl;//没有敌人,至少派一个人炸桥
        else cout<<ans<<endl;
    }
    
    return 0;
}

二分图

染色法判定二分图

#include<bits/stdc++.h>
using namespace std;

const int N = 2e5+5;
int head[N],e[N],ne[N],color[N],idx;
void add(int a,int b){
    e[idx]=b;ne[idx]=head[a];head[a]=idx++;
}

bool dfs(int u,int c){
    color[u]=c;
    for(int i=head[u];~i;i=ne[i]){
        int j=e[i];
        if(!color[j]){
            if(!dfs(j,3-c)) return false;
        }
        else if(color[j]==c) return false ;
    }
    return true;
}

int main(){
    int n,m,a,b;
    cin>>n>>m;
    memset(head,-1,sizeof head);
    while(m--){
     cin>>a>>b;
     add(a,b);
     add(b,a);
    }
    bool flag = true;
    for(int i=1;i<=n;i++){
        if(!color[i]){
            if(!dfs(i,1)){ flag=false;break; }
        }
    }
    if(flag) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
    return 0;
}

欧拉路径


欧拉路径: 如果存在一条通路包含图中所有的边,则该通路称为欧拉通路。
欧拉回路: 如果欧拉路径是一条回路,则称其为欧拉回路。
对于无向图:
 1 存在欧拉路径的充要条件 : 度数为奇数的点为0或2。
 2 存在欧拉回路的充要条件 : 度数为奇数的点为0。
对于有向图:
 1 存在欧拉路径的充要条件 : 图中奇度点的数量为0或2。
 2 存在欧拉回路的充要条件 : 所有点的度数为偶数。


AcWing 1184. 欧拉回路
题意: 给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。如果无法一笔画出欧拉回路,则输出一行:NO。否则,输出一行:YES,接下来一行输出 任意一组 合法方案即可。
代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+5, M = 4e5+5;
int n,m,type;
int head[N],e[M],ne[M],w[M],idx;
int din[N],dout[N],ans[M],cnt;
bool vis[M];

void add(int a,int b,int c){
    e[idx]=b,w[idx]=c,ne[idx]=head[a],head[a]=idx++;
}

void dfs(int u)
{
    for(int &i=head[u];~i;)//使用引用,可以直接在i=ne[i]中,将边删掉;
    {
        if(vis[i])//如果该条边使用过了,就直接删去,可以防止再次遍历,从而降低复杂度
        {
            i=ne[i];
            continue;
        }

        vis[i]=true;
        if(type==1) vis[i^1]=true;//无向图中的反向边也标记一下;

        int t;
        if(type==1){
            t=i/2+1;//转化为边的编号
            if(i&1) t=-t;//如果是反向边
        }
        else t=i+1;

        int j=e[i];
        i=ne[i];//边用过之后直接删了
        dfs(j);
        ans[++cnt]=t;//从下往上将点输入到路径中,因为从上往下的过程中,可能边路有些环并没有被顾虑到
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    memset(head,-1,sizeof head);
    cin>>type>>n>>m;
    for(int i=1;i<=m;i++){
        int a,b;
        cin>>a>>b;
        add(a,b,i);
        if(type==1) add(b,a,-i);
        dout[a]++;din[b]++;
    }
    
    if(type == 1){//无向图求欧拉回路
        for(int i=1;i<=n;i++){
            if((din[i]+dout[i]) & 1){//不符合无向图欧拉路径的充要条件
                cout<<"NO"<<endl;
                return 0;
            }
        }
    }
    else{//有向图求欧拉回路
        for(int i=1;i<=n;i++){
            if(din[i] != dout[i]){//不符合有向图欧拉路径的充要条件
                cout<<"NO"<<endl;
                return 0;
            }
        }
    }
    
    for(int i=1;i<=n;i++){
        if(head[i]!=-1){//找到一条有边的点,开始跑欧拉路径的算法
            dfs(i);
            break;
        }
    }
    if(cnt < m){//求出的欧拉回路,没有包含所有边
        cout<<"NO"<<endl;
    }
    else{
        cout<<"YES"<<endl;
        for(int i=cnt;i;i--) cout<<ans[i]<<" ";
        cout<<endl;
    }
    return 0;
}

AcWing 1124. 骑马修栅栏
输出所有欧拉路径中字典序最小的答案(输入数据保证至少有一个解。)

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510;

int n = 500, m;
int g[N][N];
int ans[1100], cnt;
int d[N];

void dfs(int u){
    for (int i= 1; i <= n; i ++ )
        if (g[u][i]){
            g[u][i] --, g[i][u] -- ;
            dfs(i);
        }
    ans[ ++ cnt] = u;
}

int main(){
    cin >> m;
    while (m -- ){
        int a, b;
        cin >> a >> b;
        g[a][b] ++, g[b][a] ++ ;
        d[a] ++, d[b] ++ ;
    }

    int start = 1;
    while (!d[start]) start ++ ;
    for (int i = 1; i <= n; i ++ )
        if (d[i] % 2){
            start = i;
            break;
        }

    dfs(start);
    for (int i = cnt; i; i -- ) printf("%d\n", ans[i]);

    return 0;
}

AcWing 1185. 单词游戏
有向图判断欧拉路径是否存在模板题
思路:
1.建好图之后判断出度和入度是否合法,
2.并且检查一下是否存在孤立的点(dfs/并查集)
即可判断出是否存在欧拉路径

#include<bits/stdc++.h>
using namespace std;

const int N = 30;
int din[N],dout[N];
int g[N][N];
int cnt;

void dfs(int u){
    for(int i=0;i<26;i++){
        if(g[u][i]){
            g[u][i]--;
            cnt++;
            dfs(i);
        }
    }
}

int main(){
    int T;
    cin>>T;
    while(T--){
        cnt = 0;
        memset(g,0,sizeof g);
        memset(din,0,sizeof din);
        memset(dout,0,sizeof dout);
        int n;
        cin>>n;
        for(int i=0;i<n;i++){
            string s;
            cin>>s;
            int a = s[0]-'a', b = s[s.size()-1]-'a';
            g[a][b]++;
            dout[a]++;din[b]++;
            
        }
        
        int s = 0, t = 0;
        bool ok = true;
        for(int i=0;i<26;i++){//根据欧拉路径的性质判断
            if(din[i] != dout[i]){
                if(din[i] == dout[i] + 1) t ++;
                else if(din[i] + 1 == dout[i]) s++;
                else { ok = false; break; }
            }
        }
        if(!(!s && !t || s == 1 && t == 1)) ok = false;
        
        s = 0;
        while(!dout[s]) s++;
        for (int i = 0; i < 26; i++)
            if(din[i] + 1 == dout[i]) { s = i; break; }//找到起点
        
        dfs(s);
        
        if(cnt < n) ok = false;//判断连通性,也可以用并查集判断
        
        puts(ok ? "Ordering is possible." : "The door cannot be opened.");
    
    }
    
}

拓扑排序

AcWing 1191. 家谱树
题意:给出 1 − i 1-i 1i 每个人的孩子的信息。输出一个序列,使得每个人的孩子都比那个人后列出。

#include<bits/stdc++.h>
using namespace std;

const int M = 10005, N = 105;
int e[M],ne[M],head[M],idx;
int ans[N],in[N],cnt,n;

void add(int a,int b){
    e[idx]=b;ne[idx]=head[a];head[a]=idx++;
}

void toposort(){
    queue<int>q;
    for(int i=1;i<=n;i++){
        if(!in[i]) q.push(i);//入度为0的点入队
    }
    while(q.size()){
        int t = q.front();
        ans[++cnt]=t;
        q.pop();
        
        for(int i=head[t];~i;i=ne[i]){
            int j=e[i];
            if(--in[j]==0) q.push(j);
        }
    }
}

int main(){
    cin>>n;
    memset(head,-1,sizeof head);
    for(int i=1;i<=n;i++){
        int x;
        while(cin>>x && x){
            add(i,x);
            in[x]++;
        }
    }
    toposort();
    for(int i=1;i<=n;i++){//输出拓扑序
        cout<<ans[i]<<" ";
    }
}

网络流

最大流

EK求最大流

#include<bits/stdc++.h>
using namespace std;

const int N = 1005, M = 2e4+5, INF = 0x3f3f3f3f;
int n,m,S,T;
int head[N],ne[M],e[M],f[M],q[M],d[N],pre[N],idx;
bool vis[N];

void add(int a,int b,int c){
    e[idx]=b,f[idx]=c,ne[idx]=head[a],head[a]=idx++;
    e[idx]=a,f[idx]=0,ne[idx]=head[b],head[b]=idx++;
}

bool bfs(){//找增广路
    memset(vis,0,sizeof vis);
    int hh = 0, tt = 0;
    q[0] = S, vis[S] = true, d[S] = INF;
    while(hh <= tt){
        int t = q[hh++];

        for(int i=head[t];~i;i=ne[i]){
            int v = e[i];
            if(!vis[v] && f[i]){
                vis[v] = true;
                d[v] = min(d[t], f[i]);
                pre[v] = i;
                if(v == T) return true;
                q[++tt] = v;
            }
        }

    }
    return false;
}

int ek(){
    int ans = 0;
    while(bfs()){
        ans += d[T];//这条增广路中能流的最大流量
        for(int i=T;i!=S;i=e[pre[i]^1]){
            f[pre[i]] -= d[T];
            f[pre[i]^1] += d[T];
        }
    }
    return ans;
}


int main(){
    memset(head,-1,sizeof head);
    cin>>n>>m>>S>>T;
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    cout<<ek()<<endl;
    return 0;
}

Dinic求最大流

#include<bits/stdc++.h>
using namespace std;
//dinic求最大流
//1.先用bfs建好分层图
//2.用dfs找到每个增广路

const int N = 1e4+5, M = 2e5+10, INF = 0x3f3f3f3f;
int n,m,S,T,idx;
int head[N],e[M],ne[M],f[M],cur[N],q[M],d[N];

void add(int a,int b,int c){
    e[idx]=b,f[idx]=c,ne[idx]=head[a],head[a]=idx++;
    e[idx]=a,f[idx]=0,ne[idx]=head[b],head[b]=idx++;
}

bool bfs(){//建立分层图
    memset(d,-1,sizeof d);
    int hh = 0, tt = 0;
    q[0] = S, d[S] = 0,cur[S] = head[S];
    while(hh<=tt){
        int t = q[hh++];
        for(int i=head[t];~i;i=ne[i]){
            int v = e[i];
            if(d[v]==-1 && f[i]){
                d[v] = d[t] + 1;
                cur[v] = head[v];
                if(v==T) return true;
                q[++tt] = v;
            }
        }
    }
    return false;
}

int find(int u,int limit){
    if(u==T) return limit;
    int flow = 0;
    for(int i=cur[u];~i && flow < limit;i=ne[i]){
        cur[u] = i;//当前弧优化
        int v = e[i];
        if(d[v] == d[u] + 1 && f[i]){
            int t = find(v, min(f[i],limit - flow));
            if(!t) d[v] = -1;
            f[i]-=t,f[i^1] += t,flow += t; 
        }
    }
    return flow;
}

int dinic(){//把多条增广路榨干
    int ans = 0, flow;
    while(bfs()) while(flow = find(S,INF)) ans += flow;
    return ans;
}

int main(){
    cin>>n>>m>>S>>T;
    memset(head,-1,sizeof head);
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    cout<<dinic()<<endl;
    return 0;
}

最大流解决二分图最大匹配问题
AcWing 2175. 飞行员配对方案问题

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 110, M = 6005, INF = 0x3f3f3f3f;
int n,m,S,T;
int head[N],e[M],ne[M],f[M],idx;
int cur[N],d[N],q[N];

/*此处省略dinic算法模板*/

int main(){
    memset(head,-1,sizeof head);
    cin>>m>>n;
    //建图
    S = 0, T = n + 1;
    for(int i=1;i<=m;i++) add(S,i,1);
    for(int i=m+1;i<=n;i++) add(i,T,1);
    int a,b;
    while(cin>>a>>b){
        if(a==-1 && b==-1) break;
        add(a,b,1);
    }
    cout<<dinic()<<endl;
    //输出最大匹配方案
    for(int i=0;i<idx;i+=2){
        if(e[i]>m && e[i]<=n && !f[i]){
            cout<<e[i^1]<<" "<<e[i]<<endl;
        }
    }

    return 0;
}

AcWing 2179. 圆桌问题
题意 m m m个单位,每个单位 r i r_i ri人, n n n个餐桌,每个餐桌最多容纳 c i c_i ci。要求同一个单位的人不在同一个餐桌就餐,输出每个单位的就餐方案

#include<bits/stdc++.h>
using namespace std;

const int N = 505, M = (270 * 150 + 500) * 2, INF = 0x3f3f3f3f;
int m,n,S,T;
int head[N],e[M],ne[M],f[M],idx;
int cur[N],d[N],q[N];

/*此处省略dinic算法模板*/

int main(){
    memset(head,-1,sizeof head);
    cin>>m>>n;
    //建图
    S = 0, T = m + n + 1;
    int x,sum = 0;
    for(int i=1;i<=m;i++){
        cin>>x;
        sum += x;
        add(S,i,x);
    }
    for(int i=1;i<=n;i++){
        cin>>x;
        add(m+i,T,x);
    }
    
    for(int i=1;i<=m;i++){
        for(int j=m+1;j<=n+m;j++){
            add(i,j,1);
        }
    }
    
    int flag = dinic() == sum ? 1 : 0;
    cout<<flag<<endl;
    
    if(flag){
        for(int i=1;i<=m;i++){
            for(int j=head[i];~j;j=ne[j]){
                if(!f[j]) cout<<e[j] - m<<" ";
            }
            cout<<"\n";
        }
    }
    
    
    return 0;
}

费用流

模板

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 5e3+5, M = 1e5+5, INF = 0x3f3f3f3f;
int n,m,S,T;
int head[N],e[M],ne[M],f[M],w[M],idx;
int d[N],limit[N],pre[M],q[N];
bool vis[N];

void add(int a,int b,int c,int d){
    e[idx]=b,f[idx]=c,w[idx]=d,ne[idx]=head[a],head[a]=idx++;
    e[idx]=a,f[idx]=0,w[idx]=-d,ne[idx]=head[b],head[b]=idx++;
}

bool spfa(){//spfa求cost最小的最短路
    memset(d,0x3f,sizeof d);
    memset(limit,0,sizeof limit);
    int hh = 0, tt = 1;
    d[S] = 0, q[0] = S, limit[S] = INF;
    while(hh != tt){
        int t = q[hh++];
        if(hh == N) hh = 0;
        vis[t] = false;

        for(int i=head[t];~i;i=ne[i]){
            int v = e[i];
            if(f[i] && d[v] > d[t] + w[i]){
                d[v] = d[t] + w[i];
                limit[v] = min(limit[t], f[i]);
                pre[v] = i;
                if(!vis[v]){
                    q[tt++] = v;
                    if(tt == N) tt = 0;
                    vis[v] = true;
                }
            }
        }
    }
    return limit[T] > 0;
 }

void ek(){
    int ans = 0, cost = 0;
    while(spfa()){
        int t = limit[T];
        cost += t * d[T];
        ans += t;
        for(int i=T;i!=S;i=e[pre[i]^1]){
            f[pre[i]] -= t, f[pre[i]^1] += t;
        }
    }
    cout<<ans<<" "<<cost<<endl;
    return ;
}

int main(){
    memset(head,-1,sizeof head);
    cin>>n>>m>>S>>T;

    while(m--){
        int a,b,c,d;
        cin>>a>>b>>c>>d;
        add(a,b,c,d);
    }
    ek();
    return 0;
}

解决二分图带权最大/小匹配问题
AcWing 2193. 分配问题
题意:有 n n n 件工作要分配给 n n n 个人做。
i i i 个人做第 j j j 件工作产生的效益为 c i j c_{ij} cij
试设计一个将 n n n 件工作分配给 n n n 个人做的分配方案。
对于给定的 n n n 件工作和 n n n 个人,计算最优分配方案和最差分配方案。

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 110, M = 6000, INF = 0x3f3f3f3f;
int m,n,S,T;
int head[N],e[M],ne[M],f[M],w[M],idx;
int q[N],d[N],limit[N],pre[N];
bool vis[N];

void add(int a,int b,int c,int d){
    e[idx]=b,f[idx]=c,w[idx]=d,ne[idx]=head[a],head[a]=idx++;
    e[idx]=a,f[idx]=0,w[idx]=-d,ne[idx]=head[b],head[b]=idx++;
}

bool spfa(){
    memset(d,0x3f,sizeof d);
    memset(limit,0,sizeof limit);
    int hh = 0, tt = 1;
    q[0] = S, d[S] = 0, limit[S] = INF;
    while(hh != tt){
        int t = q[hh++];
        if(hh == N) hh = 0;
        vis[t] = false;

        for(int i=head[t];~i;i=ne[i]){
            int v = e[i];
            if(f[i] && d[v] > d[t] + w[i]){
                d[v] = d[t] + w[i];
                limit[v] = min(limit[t], f[i]);
                pre[v] = i;
                if(!vis[v]){
                    q[tt++] = v;
                    if(tt == N) tt = 0;
                    vis[v] = true;
                }
            }
        }
    }
    return limit[T] > 0;
}

int ek(){
    int cost = 0;
    while(spfa()){
        int t = limit[T];
        cost += t * d[T];
        for(int i=T;i!=S;i=e[pre[i]^1]){
            f[pre[i]] -= t, f[pre[i]^1] += t;
        }
    }
    return cost ;
}


int main(){
    int x;
    memset(head,-1,sizeof head);
    cin>>n;
    S = 0, T = n * 2 + 1;
    for(int i=1;i<=n;i++) add(S,i,1,0);
    for(int i=1;i<=n;i++) add(n+i,T,1,0);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>x;
            add(i,n+j,1,x);
        }
    }
    cout<<ek()<<endl;

    for(int i=0;i<idx;i+=2){
        f[i] += f[i^1] , f[i^1] = 0;
        w[i] = -w[i], w[i^1] = -w[i^1];
    }
    cout<<-ek()<<endl;

    return 0;
}

ABC G - Dream Team
题意:有 n n n 人,每个人有三个属性:学校,擅长科目,能力值,求组建一支梦之队最多有多少人,假设最多有k人,并求出只有 1 − k 1-k 1k人时,队员的最大能力值,队伍要求满足中所有人来自的学校和擅长的学科都不同.
思路:把学校和学科看作点,把一个人看成匹配边,能力值看作边权,其实就是求有 i \bm{i} i 条匹配边的最优匹配.可以用费用流解决.此外题目要求输出匹配数为 1 , 2... k 1,2...k 1,2...k 个匹配时的最优匹配.在 s p f a spfa spfa费用流算法中一次 s p f a spfa spfa只会找到一条费用最小的增广流,所以每次增广之后得到的费用就对应匹配数为 1 , 2... k 1,2...k 1,2...k 个匹配时的答案.

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 310, M = 1e5+5, INF = 0x3f3f3f3f;
int m,n,S,T;
int head[N],e[M],ne[M],f[M],w[M],idx;
int q[N],limit[N],pre[N];
ll d[N];
bool vis[N];

/*此处省略add()加边函数,spfa()找费用最小的增广路*/

void ek(){
    ll cost = 0;
    vector<ll>ans;
    while(spfa()){
        int t = limit[T];
        cost += t * d[T];
        for(int i=T;i!=S;i=e[pre[i]^1]){
            f[pre[i]] -= t, f[pre[i]^1] += t;
        }
        ans.push_back(-cost);
    }

    cout<<ans.size()<<endl;
    for(auto x : ans) printf("%lld\n",x);
}

int main(){
    memset(head,-1,sizeof head);
    cin>>n;
    S = 0 , T = 301;
    for(int i=1;i<=150;i++) add(S,i,1,0);
    for(int i=151;i<=300;i++) add(i,T,1,0);
    while(n--){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,150+b,1,-c);
    }
    ek();
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值