网络流学习笔记

网络流学习笔记

前言:从 2022.12.23 到 2023.1.10,学了一年网络流(狗头,是时候总结一下了,当然之后肯定还会再刷网络流的。

upd 2024.7.4:修改了一些排版和规范。写的很烂,还没有补充修改一些知识,所以可能还是有点晦涩难懂。

目录#

1. 网络流

这边罗列一下网络的定义和流的相关性质。

网络:一张有向图 G=(V,E)

对于每条边 (u,v) 都有一个容量 c(u,v),网络中有两个重要的点,源点 s 和汇点 t

1.1 流#

每条边有流量 f(u,v),有如下三个性质:

  1. 容量限制,f(u,v)c(u,v),也就是这条边经过的流量不能超过边本身的容量。
  2. 斜对称性:f(u,v)=f(v,u)(v,u) 的流量是 (u,v) 流量的相反数。
  3. 流守恒性:从这个点流进来的流量等于从这个点流出去的流量,从源点流出的流量等于汇点流入的流量,不会有流量凭空消失。

剩余容量:c(u,v)f(u,v)

2. 最大流/最小割

2.1 最大流#

2.1.1 问题#

给定一张网络,有源点和汇点,求从源点流到汇点的最大流量。

2.1.2 算法#

这边就要引入网络流算法了,有 EK 算法和 dinic 算法,讲一下两个算法的思路。

EK 算法:每次用 bfs 遍历网络,途中要保证当前剩余容量不为零(这种路径叫做增广路),判断当前是否还有从源点到汇点的路,把路径记录下来,枚举路径中的最小流量,这就是这条路径最多能经过的流量,同时路径每条边都要减去最小流量。重复此操作,直到没有从源到汇的流量为止。复杂度 O(nm2)

dinic 算法:发现 EK 算法每次 bfs 只能找到一条增广路,有没有一次 bfs 能找到多条路径的方法?我们只需要在 bfs 的同时记录最短路/分层图,disv=disu+1,这样在记录答案时就能通过 dis 数组是否有这条路径,这样就能一次找多条路径。复杂度 O(n2m)

2.1.3 原理#

这边我先讲了算法,大家可能会疑惑,为什么这样子找的路径一定是最大流的路径呢?如果只是单纯的找路径,肯定不能保证一定是正确的路径,所以这里用了一个方法,建反向边,这边举一个例子:

一张网络,源点 1,汇点 4

显然这张网络的最大流是 2,但如果我们找到的是这条路径,显然找不到第二条增广路了。我们给每条边添加一条反向边,如图:

当我们这条边流过多少流量,原边减去流量,反边加上流量,如图:

我们可以发现,在当前包括反向边的网络中,还有一条增广路:

也就是这一条,我们经过 (3,2) 这条反向边的同时,给它的反向边,也就是 (2,3) 这条原边加上流量,当我们把所有反向边去掉之后,这条原本走错的边就还原了,同时网络也经过了正确的路径,流量为 2。反向边就是达到了这样一个反悔的效果。

2.1.4 注意事项#

网络流一般点少边多,也就是稠密图中,dinic 算法一般不会被卡,而 EK 算法容易被卡。

注意初始时,链式前向星的 cnt 要初始成 1,这样反向边就可以表示成当前边 ^ 1。

2.1.5 代码#

EK 算法:

bool bfs(){
	queue<ll> q;
	memset(pre, 0, sizeof(pre));
	for(int i = 1; i <= n; i++) vis[i] = 0;
	vis[s] = 1;
	q.push(s);
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(int i = h[u]; i; i = e[i].nxt){
			int v = e[i].to;
			if(vis[v] || e[i].w == 0) continue;
			vis[v] = 1;
			pre[v].v = u;
			pre[v].nxt = i;
			if(v == t) return 1;
			q.push(v);
		}
	}
	return 0; 
}
ll EK(){
	ll ans = 0, minn = 0x3f3f3f3f;
	while(bfs()){
		minn = 0x3f3f3f3f;
		for(int i = t; i != s; i = pre[i].v){ //遍历路径
			minn = min(minn, e[pre[i].nxt].w);
		}
		for(int i = t; i != s; i = pre[i].v){
			e[pre[i].nxt].w -= minn;
			e[pre[i].nxt ^ 1].w += minn; //反向边
		}
		ans += minn;
	}	
	return ans;
}

dinic 算法:

bool bfs(){
	queue<int> q;
	for(int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
	dis[s] = 0;
	q.push(s);
	now[s] = h[s];
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(int i = h[u]; i; i = e[i].nxt){
			int v = e[i].to;
			if(e[i].w > 0 && dis[v] == 0x3f3f3f3f){
				dis[v] = dis[u] + 1; //分层
				q.push(v);
				now[v] = h[v];
				if(v == t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u, int sum){
	if(u == t) return sum;
	int minn, ans = 0;
	for(int i = now[u]; i && sum; i = e[i].nxt){
		now[u] = i; //当前弧优化
		int v = e[i].to;
		if(e[i].w && dis[v] == dis[u] + 1){
			minn = dfs(v, min(e[i].w, sum));
			if(minn == 0) dis[v] = 0x3f3f3f3f; //这条路径已经流不出去了
			e[i].w -= minn;
			e[i ^ 1].w += minn;
			ans += minn;
			sum -= minn;
		}
	}
	return ans;
}
int dinic(){
	int ans = 0;
	while(bfs()){ 
		ans += dfs(s, 0x3f3f3f3f); 
	} 
	return ans;
}

2.2 最小割#

:将网络中的点划分成 ST 两个集合,源点 sS,汇点 tT,从 S 连向 T 的所有边就叫做割,割的权值是所有边的容量和。

2.2.1 问题#

给定一张网络,有源点和汇点,求用最少的边,覆盖从源点到汇点的所有路径,也就是求最小的割。

通俗一点,就是割掉最少的边,使得 st 不连通。

最大流/最小割定理:可以证明,最大流 = 最小割,即从 st 的最大流量等于从 st 的最小割权值。解释是最大流和最小割本质上在线性规划中是对偶的,这边笔者不会详细证明,如果想听证明的可以看看这个视频 [算法竞赛入门] 网络流基础:理解最大流/最小割定理 (蒋炎岩),挺清楚的。

割所能表达的意义之后会讲,所以,跑一遍最大流就行了。

3. 最小费用最大流

3.1 问题#

给定一张网络,有源汇点,每条边有 w(u,v) 权值,表示当前边流经 1 流量所需的费用。

在保证最大流的情况下,让费用最小。

我们只需要在最大流算法的基础上,把找增广路的 bfs 改成 spfa 就可以了,每次我们都找到当前剩余流量中所需费用最小的增广路,就能保证在没有增广路的最后,所需费用最小。

基于 dinic 算法的代码:

bool bfs(){
    queue<int> q;
    for(int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f, vis[i] = 0;
    vis[s] = 1;
    dis[s] = 0;
    q.push(s);
    while(!q.empty()){
        int u = q.front();
        vis[u] = 0;
        q.pop();
        for(int i = h[u]; i; i = e[i].nxt){
            int v = e[i].to;
            if(e[i].w && dis[v] > dis[u] + e[i].c){
                dis[v] = dis[u] + e[i].c;
                if(!vis[v]){
                    vis[v] = 1;
                    q.push(v);
                }
            }
        }
    }
    return dis[t] != 0x3f3f3f3f;
}
int dfs(int u, int sum){
    if(u == t) return sum;
    int minn, ans = 0;
    vis[u] = 1;
    for(int i = now[u]; i && sum; i = e[i].nxt){
        int v = e[i].to;
        now[u] = i; //当前弧优化
        if(e[i].w && dis[v] == dis[u] + e[i].c && !vis[v]){
            minn = dfs(v, min(e[i].w, sum));
            e[i].w -= minn;
            e[i ^ 1].w += minn;
            sum -= minn;
            ans += minn;
            cost += minn * e[i].c;
        }
    }
    if(!minn) dis[u] = 0x3f3f3f3f; 
    vis[u] = 0;
    return ans;
}
int dinic(){ //不知道叫dinic还是zkw,应该叫zkw
    int ans = 0;
    while(bfs()){
        memcpy(now, h, sizeof(h));
        ans += dfs(s, 0x3f3f3f3f);
    }
    return ans;
}

4. 二分图相关知识

通过构造网络流,能够解决一部分二分图题,所以这边讲讲二分图的相关理论。

4.1 二分图#

二分图:将图中的节点分为两个集合,使得所有边的两个端点不在同一集合的图就叫二分图。

性质:

  1. 二分图中不存在长度为奇数的环。

4.2 二分图最大匹配#

4.2.1 问题#

给定一张二分图,两个集合各有 n 个点,共有 m 条边,从中选若干条边,使得两个集合之间两两匹配的数量最多,每个点只能匹配一次,求最大匹配数是多少。

4.2.2 做法#

题目可以转化为网络流解决:

发现没有源汇点,所以我们自己构造一组虚拟源汇点 st,源点连向二分图其中一个集合的所有点,流量是 1,表示每个点只能用一次;另一个集合的所有点连向汇点,流量为 1,表示每个点只能被匹配一次。

如果 (u,v) 之间有连边,那就从一个集合的 u 连向另一个集合的 v,流量为 1inf,因为这里的边只代表两个之间可以匹配,并且每个点连的次数在源汇点连边时就已经限制了。

连完边大概是这样:

这样,我们就可以对源汇点跑最大流了,最大流就是最大匹配数。

值得注意的是,用网络流跑二分图最大匹配的复杂度是 O(nm)

4.3 二分图最小点覆盖#

4.3.1 问题#

从给定二分图中最少要选多少个点,使得每条边至少有一个端点被选中。

4.3.2 做法#

首先给出结论:最小点覆盖 = 最大匹配。

证明:

假设最小点覆盖为 n,所以所有边 n,我们一定能构造出匹配为 n 的匹配,且 最大匹配,所以最小点覆盖 最大匹配。

假设最大匹配为 n,所以边至少为 n,并且端点不同,我们要覆盖这 n 条边,至少要用 n 个点,所以最小点覆盖 最大匹配。

综上,最小点覆盖 = 最大匹配。

最小点覆盖可以解决这类问题:问至少要选多少个点才能完成所有条件(边)。

4.4 二分图最大独立集#

4.4.1 问题#

从给定二分图中选出最多的点,并且两两之间没有边相连。

4.4.2 做法#

结论:最大独立集 =n 最小点覆盖。

这个问题可以转化成二分图中选一个点,与它相连的所有点就都不能选,最多能选多少点。

这个其实就是选一个,不能选另一个的模型,容易想到最小割解决。

构造的网络和最大匹配一样,跑完最大流后,我们就求出了最小割,用 n 最小割 就是答案。

为什么呢?我们割掉一个点与源点或汇点相连的边,就表示不选这个点,为了让源汇点不连通,对于每一条路径,要么割掉源点那一条,要么割掉汇点那一条,那么我们用最少的点让源汇点不连通,剩下的点肯定是互不连通的,因此最大独立集 =n 最小割。

也可以用最小点覆盖来理解,我们用最小点覆盖了所有边,对于剩下的点,两两之间肯定不相连,反证,如果相连了,那最小点覆盖就少了这条边了,所以两两不相连,所以最大独立集=n 最小点覆盖。

4.5 最大权值闭合子图#

4.5.1 前言#

这个东西不是二分图,但解决这个问题需要构造一张二分图。

4.5.2 问题#

给定一张有向图,点权有正有负,选一个点集,要求点集中每个点连出的边连向的点都在点集中,求权值最大的点集。

4.5.3 做法#

建图方法:我们可以将点权为正的点与源点相连,边权是点权;点权为负的点与汇点相连,边权是点权的相反数,原来的边只连正到负的边,边权是 inf,这样我们就把点权转化到了边上,容易想到最小割。

先将答案加上所有正权边,表示假设全选了正权点,我们对构造的图求最小割,使 st 不连通,对于一条从源点到汇点的边,有这几种情况:

  1. 割掉从源点到正权点的边,代表不选正权点,因为原来我们已经计入正权点的贡献了,同时原图中与它相连的负权点的边,也就是负权点到汇点的边就不会被割,相当于不选负权点。
  2. 割掉从负权点到汇点的边,代表选负权点,同时原图中与它相连的正权点的边,也就是源点到正权点的边就不会被割,相当于选了正权点,因为开始时我们就已经把正权点计入贡献里了。

我们发现这其实就是要么选这个,要么选那个的最小割问题了,割完后源汇点是不连通的,就不会发生矛盾,答案其实就是所有正权点权值减去最小割。

结论:闭合子图最大权值 = 正权点之和 最小割。

5. 上下界网络流

5.1 简介#

唯一与原来的网络流不同的是,这次的容量多了个下限 b(u,v),表示这条边至少要流经多少的流量,所以图中的每条边都要满足 b(u,v)f(u,v)c(u,v)

有了这个限制,我们构造的网络就不一定是合法的了,所以我们就把上下界网络流分成五部分:

  1. 无源汇上下界可行流
  2. 有源汇上下界可行流
  3. 有源汇上下界最大流
  4. 有源汇上下界最小流
  5. 有源汇上下界最小费用可行流

5.2 无源汇上下界可行流#

可行流需要满足流量守恒,我们假设每条边都已经流了流量为下限的流量,这时候不一定网络是合法的,有两种不合法的可能:

  1. 对于某一个点,流进去的总流量小于流出去的总流量,说明有一些流量被这个点私吞了。
  2. 对于某一个点,流进去的总流量大于流出去的总流量,说明这个点很大方,多流了一些流量。

对于这些点,有些需要再流出一些给别人,有些需要被流进一些流量,我们可以这样构图:

ini 表示流进 i 点的总流量,outi 表示流出 i 点的总流量。

  1. 如果 iniouti>0,说明流进去的总流量小于流出去的总流量,我们连一条从源点到 i 点的边,边权为 iniouti,表示从源点吐出要给别人的流量。
  2. 如果 iniouti<0,说明流进去的总流量大于流出去的总流量,我们连一条从 i 点到汇点的边,边权为 outiini,表示还需要让别人给多少流量。

原来的边照样连,但是边权是 上限 下限,表示能通过的边。

这样我们从虚拟源汇点跑一次最大流,如果是满流,也就是源点要流出去的流量全都到了汇点,就说明是有合法的流的。

#include <bits/stdc++.h>
using namespace std;
inline int read(){
    int x = 0, f = 1;
    char c = getchar();
    while(c < '0' || c > '9'){
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + (c - '0');
        c = getchar();
    }
    return x * f;
}
int n, m, cnt = 1, s, t, sum;
int dis[100010], now[100010], h[100010], in[100010], low[100010];
struct node{
    int to, nxt, w;
}e[500010];
void add(int u, int v, int w){
    e[++cnt].to = v;
    e[cnt].w = w;
    e[cnt].nxt = h[u];
    h[u] = cnt;
}
void adde(int u, int v, int w){
    add(u, v, w);
    add(v, u, 0);
}
bool bfs(){
    queue<int> q;
    for(int i = 1; i <= t; i++) dis[i] = 0x3f3f3f3f;
    dis[s] = 0;
    now[s] = h[s];
    q.push(s);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = h[u]; i; i = e[i].nxt){
            int v = e[i].to;
            if(e[i].w && dis[v] == 0x3f3f3f3f){
                dis[v] = dis[u] + 1;
                now[v] = h[v];
                q.push(v);
                if(v == t) return 1;
            }
        }
    }
    return 0;
}
int dfs(int u, int val){
    if(u == t) return val;
    int minn, ans = 0;
    for(int i = now[u]; i && sum; i = e[i].nxt){
        int v = e[i].to;
        now[u] = i;
        if(e[i].w && dis[v] == dis[u] + 1){
            minn = dfs(v, min(e[i].w, val));
            if(!minn) dis[v] = 0x3f3f3f3f;
            e[i].w -= minn;
            e[i ^ 1].w += minn;
            ans += minn;
            val -= minn;
        }
    }
    return ans;
}
int dinic(){
    int ans = 0;
    while(bfs()){
        ans += dfs(s, 0x3f3f3f3f);
    }
    return ans;
}
int main(){
    n = read(), m = read();
    s = n + 1, t = s + 1;
    for(int i = 1; i <= m; i++){
        int u = read(), v = read(), lo = read(), up = read();
        in[u] -= lo;
        in[v] += lo; //记录流入和流出
        low[i * 2] = lo; //记录下限
        adde(u, v, up - lo);
    }
    for(int i = 1; i <= n; i++){
        if(in[i] > 0){
            adde(s, i, in[i]);
            sum += in[i];
        }
        else{
            adde(i, t, -in[i]);
        }
    }
    int ans = dinic();
    if(ans != sum){
        cout << "NO" << endl;
        return 0;
    }
    cout << "YES" << endl;
    for(int i = 2; i <= m * 2; i += 2){
        cout << e[i ^ 1].w + low[i] << endl; //每条边的流量就是原本流的下限加上反向边的流量
    }
    return 0;
}

5.3 有源汇上下界可行流#

与无源汇不同的就是图本身有了源汇点,而源汇点是不需要满足流量平衡的。

其实我们只需要汇点向源点连一条容量为 inf 的边就行了,表示源点流出的流量与汇点流入的流量平衡,这样问题就变成了无源汇了,只需要再建新源点 S 和新汇点 T,跑最大流就可以了。

5.4 有源汇上下界最大流#

最大流怎么求呢,跟无源汇最大流一样跑最大流,其实 t 连到 s 的反向边就是一个可行流,我们只需要把源汇点设为原本的 st,跑一遍残余网络就行了,答案是可行流加上残余网络还能跑多少流。

#include <bits/stdc++.h>
using namespace std;
inline int read(){
    int x = 0, f = 1;
    char c = getchar();
    while(c < '0' || c > '9'){
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + (c - '0');
        c = getchar();
    }
    return x * f;
}
int n, m, s, t, cnt = 1, S, T, sum;
int dis[100010], h[100010], now[100010], in[100010], low[100010];
struct node{
    int to, nxt, w;
}e[500010];
void add(int u, int v, int w){
    e[++cnt].to = v;
    e[cnt].w = w;
    e[cnt].nxt = h[u];
    h[u] = cnt;
}
void adde(int u, int v, int w){
    add(u, v, w);
    add(v, u, 0); 
}
bool bfs(){
    queue<int> q;
    for(int i = 1; i <= T; i++) dis[i] = 0x3f3f3f3f;
    dis[S] = 0;
    now[S] = h[S];
    q.push(S);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = h[u]; i; i = e[i].nxt){
            int v = e[i].to;
            if(e[i].w && dis[v] == 0x3f3f3f3f){
                dis[v] = dis[u] + 1;
                now[v] = h[v];
                q.push(v);
                if(v == T) return 1; 
            }
        }
    } 
    return 0;
}
int dfs(int u, int val){
    if(u == T) return val;
    int minn, ans = 0;
    for(int i = now[u]; i && val; i = e[i].nxt){
        int v = e[i].to;
        now[u] = i;
        if(e[i].w && dis[v] == dis[u] + 1){
            minn = dfs(v, min(e[i].w, val));
            if(!minn) dis[v] = 0x3f3f3f3f;
            e[i].w -= minn;
            e[i ^ 1].w += minn;
            val -= minn;
            ans += minn;
        }
    }
    return ans;
}
int dinic(){
    int ans = 0;
    while(bfs()){
        ans += dfs(S, 0x3f3f3f3f);
    }
    return ans;
}
int main(){
    n = read(), m = read(), s = read(), t = read();
    S = n + 1, T = S + 1;
    for(int i = 1; i <= m; i++){
        int u = read(), v = read(), lo = read(), up = read();
        in[u] -= lo;
        in[v] += lo;
        low[2 * i] = lo;
        adde(u, v, up - lo);
    }
    for(int i = 1; i <= n; i++){
        if(in[i] > 0){
            adde(S, i, in[i]);
            sum += in[i];
        }
        else{
            adde(i, T, -in[i]);
        }
    }
    adde(t, s, 0x3f3f3f3f);
    if(dinic() != sum){
        cout << "please go home to sleep" << endl;
        return 0;
    }
    int ans = e[cnt].w;
    S = s, T = t;
    e[cnt - 1].w = e[cnt].w = 0;
    cout << ans + dinic() << endl;
    return 0;
}

5.5 有源汇上下界最小流#

做法与有源汇上下界最大流一样,唯一不同的就是源汇点设为原本的 ts,答案是可行流减去残余网络还能跑的流量。

6. Trick

6.0 前言#

这些只是我遇到的一些题目建图的技巧,并不是所有。

6.1 拆点#

如果题目对点有限制,比如说点只能经过几次,我们就可以把一个点拆成一头一尾两个点,连边时,起点的尾巴连向终点的头,自己本身的头连尾巴,容量是限制的次数。

6.2 二分+网络流#

如果答案不能直接求出,而它本身的大小对于网络的连边也有一定的限制,那么可以把它作为二分的对象,每一次二分,建一张新图,跑一遍网络流,如果还能满足条件,那就还能继续往某个方向寻找答案。

6.3 分层图#

如果随着流量流向下一个点,一个东西会不断的减少,那么我们就可以以这个东西分层,比如时间,我们可以经过时间的大小,建不同层,从下层连上层,代表时间的经过。

P4009 汽车加油行驶问题

发现时间这个限制,可以用时间分层,如果油用完了,那就只能原地建加油站,同时连边到第一层,也就是满油的层。

6.4 动态开点#

如果把所有的情况一下子全开了,会超出内存限制,并且一些点在某些情况下不需要建出来,是多余的,我们就可以边统计贡献,边开点。

P2050 [NOI2012] 美食节

P2053 [SCOI2007] 修车 的数据加强版,考虑将厨师每个时刻做菜的贡献不同,将厨师分成若干个点,分的点太多,会超内存,所以用动态开点,用时间换空间。

6.5 取物连重边#

如果一个东西,只有在第一次取的时候有贡献,之后虽然可以经过,但是没有贡献,考虑连两条边,一条容量为 1,价值为 v;一条容量为 inf,价值为 0,就可以满足这个条件了。

P2045 方格取数加强版

6.6 黑白染色#

比如条件是一个点用了,某些点就不能用,这是明显的二分图问题,但图中都是正权点,可以观察性质,发现同一类的点只能限制另一类的点,就可以黑白染色。

P3355 骑士共存问题

发现 (i+j)mod2=0 的点只能限制 (i+j)mod2=1 的点,考虑用奇偶性染色,把图变成二分图来求解。

6.7 优化建边#

如果二分图里,直接两两建边,边数太多,会 TLE,我们可以用其他的相同点来连边,优化边数。

P2065 [TJOI2011] 卡片

这题直接建边只能得 70pts,考虑优化边数,发现两个之间能够连边的共同之处是有公因数,所以把公因数当做中转站,路径为蓝色卡片 公因数 红色卡片。

6.8 建虚拟点#

如果某些贡献需要多个东西同时满足共同的条件,可以考虑建一个新点,将这个点连向需要满足条件的边,并且答案是可以用总贡献 最小割得到。

P1646 [国家集训队]happiness

源汇点分别表示文理科,朋友之间如果共同是文科或理科有额外的贡献,这个贡献可以建虚拟点,连向一对朋友,源电或汇点连向虚拟点,跑最小割,用总贡献减去需要舍弃的最小贡献就是答案。

(附)最小割树

笔者还没写......

没事,最小割树就是一颗记录着原本网络不同源汇点的割大小的树。

朴素的做法是,每次询问源点 u,汇点 v,跑一次最小割,复杂度很高,肯定过不了。

最小割树的算法的思想就是分治 + 网络流,我们把原本的网络看作一个集合,随便从集合中取两个点 uv,跑最小割,最小割会将网络分成新的两个集合,在树中体现出来就是 uv 连一条权值是最小割的边,重复这个操作,直到变成 n 个集合为止。

如果我们要查询随便两个点,就可以通过最小割树查询,由于树上点到点的路径是唯一的,两个点的最小割就是树中两个点路径上的最小边权,感性理解一下,要将 uv 分开,uv 路径上的边哪一条割开都可以,我们要让割最小,就用路径上的最小割就行。

END

2023.1.10 写了一天,终于写完了,这篇学习笔记肯定是参考了很多神犇的博客和视频,包括 OIwiki,网络流还有一些没怎么学,比如最小割树......

总之 ,写完啦!

posted @   Fire_Raku  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示