搜索与图论

搜索与图论

1.深度优先搜索

  • 用的是栈
  • 空间是O(h)
  • 不具有最短路性质

回溯:恢复现场

剪枝:不是最优解,或者不是可行解,就直接剪枝(不搜索)

2.广度优先搜索

  • 一层一层搜索
  • 用的是队列
  • 空间是O( 2 h 2^h 2h),指数级别
  • 具有最短路性质(第一次搜到的符合条件的点,一定是最近的点)
  • 边权都是 1 的时候,才能用 B F S BFS BFS解决最短路问题
queue = 初始化
while queue 不空
{
    t = 队头
    拓展 t
}

3.图的存储

  • 树是一种无环连通图

邻接矩阵

  • 空间复杂度高
  • 适合稠密图

邻接表

  • 其实就是哈希表的拉链法

还可以用结构体存边(边的两端点及边的权重)

4.图的遍历

  • 时间复杂度 O ( n + m ) O(n+m) O(n+m) n n n 表示点数, m m m 表示边数

深度优先遍历

  • 基本代码框架
int dfs(int k)
{
	st[k]=1;
    
    for(int i = h[k];i!=-1;i=ne[i])
    {
		int j = e[i];
        if(!st[j])
            dfs(j);
    }
}

广度优先遍历

  • 基本代码框架
queue<int> q;
q.push(1);
st[1]=1;

while(!q.empty())
{
	int t = q.front();
    q.pop();
    
    for(int i = h[t];i!=-1;i=ne[i])
    {
		int j = e[i];
        if(!st[j])
        {
			st[j]=1;
            q.push(j);
        }
    }
}

5.拓扑排序

  • 必须是有向图

  • 按照拓扑序排好之后,所有的边都是从前指向后的

  • 并不是所有图都有拓扑序(有环)(有向无环图一定存在拓扑序列

  • 时间复杂度是 O ( n + m ) O(n+m) O(n+m)

int topsort()
{
	for(int i = 1;i<=n;i++)
    {
		if(!d[i])			//d[i] 表示 点 i 的入度
            q[++tt]=i;		//即把入度为 0 的点存入队列
    }
    
    while(hh<=tt)
    {
		int t = q[hh++];
        
        for(int i = h[t];i!=-1;i=ne[i])
        {
			int j = e[i];	//遍历新的结点
            d[j]--;			//该节点的入度--
            if(!d[j])		//如果该结点的入度变为0,就是存入队列
            {
                q[++tt] = j;
            }
        }
    }
    if(tt == n-1)		//如果队列里面放了n-1个,即前n-1个都满足条件,所以最后一个也满足条件,所以符合拓扑排序
        return 1;
    return 0;
}

6.最短路问题

难点在建图

单源最短路

所有边权都是正数
朴素迪杰斯特拉算法( d j i k s t r a djikstra djikstra)
  • 时间复杂度 O ( n 2 ) O(n^2) O(n2),与边数没有关系,适合稠密图

思路:

  • 设置 dist[i]数组 代表第 i i i 个点到起点的最短距离

  • 设置f[i] 数组 代表已经确定第 i i i个点是 1 1 1~ n n n的最短路上的点

  • 首先初始化 dist[N] 数组为正无穷,并令 dist[1] = 0

  • 然后遍历 n − 1 n-1 n1 遍,(因为剩下的最后一个点没有必要作为基点更新最短路了

  • 每次遍历都要先找到满足条件的点,然后用来更新

找到满足条件的点:没有被访问过并且距离可以变小

int t = 0;
for (int j = 1; j <= n; j++)
{
	if (!f[j] && dist[t] > dist[j])
		t = j;
}

更新

for (int j = 1; j <= n; j++)
{
	dist[j] = min(dist[j], dist[t] + g[t][j]);
}
f[t] = 1;

代码:

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

int n, m;
int g[N][N];
int dist[N];
bool f[N];

int dijkstra()
{
	dist[1] = 0;

	for (int i = 1; i < n; i++)
	{
		int t = 0;
		for (int j = 1; j <= n; j++)
		{
			if (!f[j] && dist[t] > dist[j])
				t = j;
		}
		for (int j = 1; j <= n; j++)
		{
			dist[j] = min(dist[j], dist[t] + g[t][j]);
		}
		f[t] = 1;
	}
	if (dist[n] > 0x3f3f3f3f/2)
		return -1;
	return dist[n];
}
int main()
{
	cin >> n >> m;
	memset(g, 0x3f, sizeof g);
	memset(dist, 0x3f, sizeof dist);
	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		g[a][b] = min(g[a][b], c);
	}
	cout << dijkstra() << "\n";
	return 0;
}
堆优化版的迪杰斯特拉算法
  • 时间复杂度是 O ( m l o g n ) O(mlogn) O(mlogn)

  • 适用于稀疏图

怎么优化?

  • 把遍历求最小值的操作,变为优先队列的操作

代码:

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

typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool f[N];
priority_queue<PII, vector<PII>, greater<PII>> q;

int dijkstra()
{
	q.push({ 0,1 });	//到1号点的距离是0,注意这里的顺序不能变!!!(因为pair<int,int> 的排序是先根据first,再根据second)
	dist[1] = 0;
	while (!q.empty())
	{
		PII t = q.top();
		q.pop();
		int b = t.second;
		if (f[b])			//如果b这个点被访问过了吗,可以直接continue
			continue;
		f[b] = 1;
		for (int i = h[b]; i != -1; i = ne[i])
		{
			int j = e[i];
            //更新
			if (dist[j] > dist[b] + w[i])
			{
				dist[j] = dist[b] + w[i];
				q.push({ dist[j],j });
			}
		}

		
	}
	if (dist[n] > 0x3f3f3f3f / 2)
		return -1;
	return dist[n];
}
void add(int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}
int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	memset(dist, 0x3f, sizeof dist);

	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c);
	}
	cout << dijkstra() << "\n";
	return 0;
}
存在负权边
B e l l m a n − F o r d Bellman-Ford BellmanFord 时间复杂度: O ( n m ) O(nm) O(nm)
  • 该算法可求得:从 1 − n 1-n 1n号点,经过 k k k 条边的最短距离

  • 如果有负环,则最短路 可能 不存在

  • 可以判断是否存在负环(但是一般用 s p f a spfa spfa来判断)

思路:

  • 遍历 k k k 次,每次找到一条边
  • 每次遍历所有边,找到可以更新的
  • 注意更新时要用到 backup[] !!!
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define N 100010
using namespace std;

int n, m, k;
int backup[N];
int dist[N];
bool f[N];
typedef struct edge
{
	int a, b, w;
}edge;

edge e[N];
int bellman_fold()
{
	dist[1] = 0;
	for (int i = 1; i <= k; i++)
	{
        //要用 backup[]来更新!!!
		memcpy(backup, dist, sizeof dist);
		for (int j = 1; j <= m; j++)
		{
			int a = e[j].a, b = e[j].b, w = e[j].w;
            //关键代码!!!
			dist[b] = min(dist[b], backup[a] + w);
		}
	}

	if (dist[n] > 0x3f3f3f3f / 2)
		return -0x3f;
	return dist[n];
}
int main()
{
	cin >> n >> m >> k;
	memset(dist, 0x3f, sizeof dist);

	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		e[i] = { a,b,c };
	}
    int t = bellman_fold();
    if(t==-0x3f)
        cout<<"impossible";
    else
        cout<<t;
    return 0;
}
S P F A SPFA SPFA 一般: O ( m ) O(m) O(m),最坏: O ( n m ) O(nm) O(nm)
  • 用队列优化 B e l l m a n − f o l d Bellman-fold Bellmanfold
  • 我们发现 B e l l m a n − f o l d Bellman-fold Bellmanfold 遍历的所有边中,并不是每个边都用得到,所以我们可以用队列存下来有用的新边,遍历这些边即可
  • f[i]=1 代表队列中存在1号结点
#include<bits/stdc++.h>
#define N 1000100
using namespace std;

typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool f[N];
queue<int> q;

int spfa()
{
	dist[1] = 0;
	q.push(1);
	f[1] = 1;//代表队列中有1号结点

	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		f[t] = 0;

		for (int i = h[t]; i != -1; i = ne[i])
		{
			int j = e[i];
			if (dist[j] > dist[t] + w[i])
			{
				dist[j] = dist[t] + w[i];
				if (!f[j])
				{
					q.push(j); f[j] = 1;
				}
			}
		}
	}

	return dist[n];
}
void add(int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}
int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	memset(dist, 0x3f, sizeof dist);

	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c);
	}
	int t = spfa();
	if (t == -1)
		cout << "impossible";
	else
		cout << t;
	return 0;
}
求负环
  • cnt[] 数组表示到达第 i i i个点的最短路,需要经过的边数

  • 所以每次更新 dist[] , cnt[]都需要 + 1 +1 +1

  • cnt[x]>=n即存在负环

  • 关键代码

if (dist[j] > dist[t] + w[i])
{
	dist[j] = dist[t] + w[i];
	cnt[j] = cnt[t] + 1;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define N 1000100
using namespace std;

typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N]; int cnt[N];
bool f[N];
queue<int> q;

int spfa()
{
	for (int i = 1; i <= n; i++)
	{
		f[i] = 1;
		q.push(i);
	}
	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		f[t] = 0;

		for (int i = h[t]; i != -1; i = ne[i])
		{
			int j = e[i];
			if (dist[j] > dist[t] + w[i])
			{
				dist[j] = dist[t] + w[i];
				cnt[j] = cnt[t] + 1;

				if (cnt[j] >= n)
					return 1;
				if (!f[j])
				{
					q.push(j); f[j] = 1;
				}
			}
		}
	}

	return 0;
}
void add(int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}
int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
//	memset(dist, 0x3f, sizeof dist);

	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c);
	}
	if (spfa())
		cout << "Yes";
	else
		cout << "No";
	return 0;
}

多源汇最短路

F l o y d Floyd Floyd 时间复杂度: O ( n 3 ) O(n^3) O(n3)
关键代码
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]);
			}
		}
	}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define N 1010
#define INF 1000000000
using namespace std;

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()
{
	cin >> 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;
		cin >> a >> b >> c;
		d[a][b] = min(d[a][b], c);
	}

	floyd();
	while (Q--)
	{
		int a, b;
		cin >> a >> b;
		int t = d[a][b];
		if (t > INF / 2)
			cout << "impossible\n";
		else
			cout << t<<'\n';
	}
	return 0;
}

7.最小生成树

普利姆算法( P r i m Prim Prim

朴素版 P r i m Prim Prim 时间复杂度: O ( n 2 ) O(n^2) O(n2)

适合于稠密图,思路跟 d i j k s t r a dijkstra dijkstra类似

  • 注意与 d i j k s t r a dijkstra dijkstra不同的是,每次更新dist[]数组时,需要更新 t t t到集合中所有点的距离,而不只是到起点
#include<bits/stdc++.h>
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;

int g[N][N];
int n, m;
int dist[N];
bool st[N];

int prim()
{
	dist[1] = 0;
	int res = 0;
	for (int i = 1; i <= n; i++)
	{
		int t = 0;
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && dist[t] > dist[j])
			{
				t = j;
				
			}
		}
		st[t] = 1;
		if (i != 1 && dist[t] > INF / 2)
			return -INF;
		if (i != 1)
			res += dist[t];
        //关键代码!!!
		for (int j = 1; j <= n; j++)
		{
			dist[j] = min(dist[j], g[t][j]);
		}
	}
	return res;
}
int main()
{
	cin >> n >> m;
	memset(dist, 0x3f, sizeof dist);
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			if (i == j)
				g[i][j] = 0;
			else
				g[i][j] = INF;
		}
	}
	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		g[a][b] = g[b][a] = min(g[a][b], c);
	}

	int t = prim();
	if (t == -INF)
		cout << "impossible\n";
	else
		cout << t;
	return 0;
}
堆优化版 P r i m Prim Prim O ( n 2 ) O(n^2) O(n2)

不常用

克鲁斯卡尔算法( K r u s k a l Kruskal Kruskal) 时间复杂度: O ( m l o g n ) O(mlogn) O(mlogn)

适用于稀疏图

  • 将所有边按照权值大小排序
  • 枚举每条边以及它的权重
  • 如果边的两顶点不连通 就把这条边并入到集合中 (并查集!!!)
#include<bits/stdc++.h>
#define N 200010
#define INF 0x3f3f3f3f
using namespace std;

int n, m;
typedef struct node
{
	int a, b, w;
}edge;
edge e[N];
int p[N];
int ans; int cnt;

bool cmp(node& x, node& y)
{
	return x.w < y.w;
}

int find(int x)
{
	if (p[x] != x)
		p[x] = find(p[x]);
	return p[x];
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		e[i] = { a,b,c };
	}
	sort(e + 1, e + 1 + n, cmp);
	for (int i = 1; i <= n; i++)
		p[i] = i;
	for (int i = 1; i <= m; i++)
	{
		int a = e[i].a; int b = e[i].b; int w = e[i].w;
        //关键代码!!!
		a = find(a), b = find(b);
		if (a != b)
		{
			p[a] = b;
			ans += w;
			cnt++;
		}
	}
	if (cnt < n - 1)
		cout << "impossible\n";
	else
		cout << ans;
	return 0;
}

8.二分图

判别是否为二分图:

染色法 O ( m + n ) O(m+n) O(m+n)

关键代码

int dfs(int u, int c)
{
	color[u] = c;
	for (int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if (!color[j])
		{
			if (!dfs(j, 3 - c))
				return 0;
		}
		else if (color[j] == c)
			return 0;
	}
	return 1;
}


for (int i = 1; i <= n; i++)
{
	if (!color[i])			//color[i]==0,表示未染色
	{
		if (!dfs(i, 1))		//dfs()返回0,表示找不到符合条件的染色方案
		{		
            f = 0; break;
        }
   	}
}

代码表示

#include<bits/stdc++.h>
#define N 1000010
#define M 1000010

using namespace std;

int n, m;
int h[N], e[N], ne[N], idx;
int color[N]; bool f = 1;

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

int dfs(int u, int c)
{
	color[u] = c;
	for (int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if (!color[j])
		{
			if (!dfs(j, 3 - c))
				return 0;
		}
		else if (color[j] == c)
			return 0;
	}
	return 1;
}
int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	for (int i = 1; i <= m; i++)
	{
		int a, b;
		cin >> a >> b;
		add(a, b);add(b,a);
	}
    
	for (int i = 1; i <= n; i++)
	{
		if (!color[i])
		{
			if (!dfs(i, 1))
			{
				f = 0; break;
			}
		}
	}
	if (!f)
		cout << "No\n";
	else
		cout << "Yes\n";
	return 0;
}

给定一个二分图,要求最大匹配: 匈牙利算法

最坏: O ( m n ) O(mn) O(mn) 实际一般远小于

成功匹配:不存在两条边公用一个点的(没有一个点有两条边连着)

  • 可以形象的表示为找对象 ( d o g e ) (doge) (doge ,具体表示就不赘述了,下面开始看代码

关键代码

//为男生x找对象
int find(int x)
{
    for(int i = h[x];i!=-1;i=ne[i])
    {
        int j = e[i];
        if(!st[j])			//如果第j个女生没有被男生x找过
        {
            st[j]=1;
            if(!match[j] || find(match[j]))		//如果j已经是之前某个男生的对象了,或者j之前的对象还有别的选择
            {
                match[j]=x;
                return 1;
            }
        }
    }
    return 0;
}


for(int i = 1;i<=n1;i++)
{
   memset(st,0,sizeof st);		//st[j]==0, 意思是 为每个男生i找对象时,还没有找到第j个女生
   if(find(i))
      res++;
}

代码:

#include<bits/stdc++.h>
#define N 100010
#define M 100010

using namespace std;

int n1, n2, m;
int h[N], e[N], ne[N], idx;
bool st[N]; int res;
int match[N];

int find(int x)
{
    for(int i = h[x];i!=-1;i=ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            st[j]=1;
            if(!match[j] || find(match[j]))
            {
                match[j]=x;
                return 1;
            }
        }
    }
    return 0;
}

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

int main()
{
	cin >> n1 >> n2 >> m;
	memset(h, -1, sizeof h);
	for (int i = 1; i <= m; i++)
	{
		int a, b;
		cin >> a >> b;
		add(a, b);
	}
	for(int i = 1;i<=n1;i++)
	{
	    memset(st,0,sizeof st);
 	   if(find(i))
 	       res++;
	}
	cout << res;
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值