图论(四大容器)拓扑排序 从入门到入坑 完整版

本文介绍了图论的基本概念,包括顶点、边和度,并详细讲解了图论中的四大容器——List、Set、Map和Queue的接口及其常见实现类。接着,文章深入探讨了图的建立方法,如邻接矩阵、邻接链表和前向星,并提供了无权值和带权值的代码实现。最后,重点讨论了拓扑排序的概念、条件、实现方法,并给出了两个具体的例题:旅行计划和最长路问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图论:

要素:

顶点(Vertex) 边(Edge) 边权(Weight)**

无向图:边数
有向图:出度和入度

树为一种比较特殊的图

四个基本容器

基本的容器有四个,分别对应四个接口:

List:按照插入的顺序保存元素,可以有重复的元素。
Set:不可以有重复的元素,可以理解为哈希表。
Queue:按照队列规则先进先出。
Map:将两个元素相关联,可以用键来查找值。

其中List、Set、Queue这三个接口继承了Collection接口。

Map则是自成一家,没有继承别人。
在这里插入图片描述

各接口的常用实现类

List:ArrayList、LinkedList
Set:HashSet、TreeSet、LinkedHashSet
Map:HashMap、TreeMap、LinkedHashMap
Queue:PriorityQueue、LinkedList

ArrayList和LinkedList这两个是实现类,实现了List接口,这两个类可能会增加一些接口没有的新方法(刷题时一般用不到)。

同理、Set、Queue、Map的实现类也一样。

1,类的定义方式:

如果需要用到一个List容器可以像下面这样子定义

ArrayList<Integer> list = new ArrayList<>();
LinkedList<Integer> list = new LinkedList<>();
List<Integer> list = new ArrayList<>();//接口实现的对象方法少一点
ArrayList<Integer> list = new ArrayList<>()//类实现的对象多一点

Set和Map和Queue的定义也和list一样。

List容器

ArrayList:底层使用数组实现的,查找快,插入数据和删除数据慢(常用)
LinkedList:底层使用链表实现的,查找慢,插入数据和删除数据快(常用)

1,创建一个List容器

List<Integer> list = new ArrayList<>()ArrayList<Character> hui=new ArrayList<>();

 ArrayList<String> ans=new ArrayList<>();
 ans.add("qiao ben zhong.\n");//可以实现加入回车
 
 ArrayList[] arrayList = new ArrayList[n];
 //每一个一维数组都要实例化
 for(int i=0;i<n;i++)
		{
			a[i]=new ArrayList<>();
		}
 for (int j = 0; j < arrayList[i].size(); j++) 

List(可用arraylist也可以用linkedlist)常用方法有

arrayList[i].get(j)//二维数组
list.add(int element)//添加元素(常用)
list.get(int index)//根据索引查找元素
list.size()//反回元素的数量(常用)
list.isEmpty()//判断是否为空(常用)
list.remove()//根据下标删除元素
list.clear();//清空元素

2,模拟栈:

		LinkedList<Integer> a=new LinkedList<>();
		a.add(12);a.add(13);a.add(133);a.add(21);
		int z=a.get(a.size()-1);//返回栈顶元素
		a.remove(a.size()-1);//清除栈顶元素
		System.out.println(a);

2,模拟队列:

		LinkedList<Integer> b=new LinkedList<>();//队列
		b.add(1);b.add(3);b.add(12);
		int x=b.poll();//返回队首元素,删除队首元素
		int y=b.peek();//返回队首元素
		b.removeFirst();//直接删除队首元素
		System.out.println(b);

Stack容器

//创建堆栈对象
			Stack stack=new Stack();
			System.out.println("aaaa,bbbb,cccc三个元素入栈");
			//向栈中压入字符串aaaa
			stack.push("aaaa");
			//显示栈中的所有元素
			printStack(stack);
			stack.push("bbbb");
			printStack(stack);
			stack.push("cccc");
			printStack(stack);
			String s=new String("aaaa");
			System.out.println("元素aaaa在堆栈的位置"+stack.search(s));
			s=new String("bbbb");
			System.out.println("元素bbbb在堆栈的位置"+stack.search(s));
			System.out.println("aaaa,bbbb,cccc三个元素出栈");
			System.out.println("元素"+stack.pop()+"出栈");
			printStack(stack);
			System.out.println("元素"+stack.pop()+"出栈");
			printStack(stack);
			System.out.println("元素"+stack.pop()+"出栈");
			printStack(stack);
			
		}

Set容器(可去重)

Set接口实现类有
HashSet:使用散列函数查找快速(也就是常说的哈希查找)(常用)
TreeSet:排序默认从小到大(去重并排序)
LinkedHashSet:使用链表维护元素的插入顺序
Set set = new HashSet<>();
可以直接取数
(缺点:取数困难,一般用新容器通过addAll()再存一遍,后取数,不知道当时怎么想的,也不知道看了哪里的资料)

Set常用方法有

set.add()//添加元素
set.remove(Object o)//删除一个对象的引用
set.clear()//清空容器
set.isEmpty()//判断容器是否为空
set.contains(Object o)//判断容器是否包含o(常用)
set.size()//返回容器中元素的数目

Map容器

Map接口实现类有
HashMap:用于快速访问,常用与哈希有关的(常用)
TreeMap:键处于排序状态
LinkedHashMap:保持元素插入的顺序

Map<String,Integer> map = new HashMap<>()Map常用方法有
map.putIfAbsent()//添加键值对,可以去重value
map.put()//添加键值对,直接覆盖
1.使用put方法添加键值对,如果map集合中没有该key对应的值,则直接添加,并返回null,如果已经存在对应的值,则会覆盖旧值,value为新的值。
2.使用putIfAbsent方法添加键值对,如果map集合中没有该key对应的值,则直接添加,并返回null,如果已经存在对应的值,则依旧为原来的值。
map.get()//取数据
int k=entry.getKey();
int v=entry.getValue();
map.remove(Object key)//删除键值对
map.remove(Object key,Object value)//删除键值对
map.size()//返回元素的数量
map.clear()//清空容器
map.isEmpty()//判断容器是否为空
map.containsKey()//判断容器是否包含某键
map.containsValue()//判断容器是否包含某值
map.put(1,11);
map.getOrDefault(key,val2);如果key无初始值,返回val2,有就直接返回初始值

```java

遍历Map:

两种方法:
1,使用 map.keySet()

Map<Integer,Integer> mp=new HashMap<>();
		mp.put(0,mp.getOrDefault(0, 0)+1);
		mp.put(3, 122);
		mp.put(5, mp.getOrDefault(5, 12));
		for(Integer it:mp.keySet())
			System.out.println(mp.get(it));

2,使用Map.Entry<> entry:变量名.entrySet()

static Map<Integer,Integer> m=new HashMap<>();
for(Map.Entry<Integer, Integer> entry:m.entrySet())
		{
			int k=entry.getKey();
			int v=entry.getValue();
			while(v-->0)
			{
				sb.append("*"+k);
			}
		}

修改value:

不同于c++。java通过覆盖换值。
使用m.put();
mc.getOrDefault(i, 0) 获取i的初始值,无就返回0.
自加操作;
mc.put(i, mc.getOrDefault(i, 0)+1);

 for(int i=0;i<nums.length;i++)
       {
    	   if(m.get(nums[i])==null) m.put(nums[i], 0);
           cnt=(cnt+m.get(nums[i]))%maxx;
        //    int value=m.get(nums[i])+1;
           m.put(nums[i], m.get(nums[i])+1);
       }

Queue容器

PriorityQueue:优先队列(常用)
LinkedList:普通队列(常用)

有些人可能有疑问
怎么Queue还和LinkedList联系起来了,其实Queue的添加队尾和删除队头不就是相等于插入链表的尾结点,和删除链表的头结点吗,所以说不需要在定义一个Queue的实现类了。

Queue常用方法有
offer()添加元素(常用)
peek()返回第一个元素(常用)
poll()返回第一个元素并删除(常用)
add()添加元素
element()返回第一个元素
remove()返回第一个元素并删除
put()添加元素
take()返回第一个元素并删除

使用迭代器:
方法:
it.hasnext()是否有下一个结点(
temp.contains(obj)某temp容器是否包含obj

Iterator it=list.iterator();
while(it.hasNext())
{
		Object obj=it.next();//会自动迭代到下一个结点)
		if(!temp.contains(obj)) temp.add(obj);	
	}

容器的遍历

在这里插入图片描述
在这里插入图片描述

static Map<Integer,Integer> m=new HashMap<>();
for(Map.Entry<Integer, Integer> entry:m.entrySet())
		{
			int k=entry.getKey();
			int v=entry.getValue();
			while(v-->0)
			{
				sb.append("*"+k);
			}
		}

容器的排序:

return -1对应着序列:(a-b (a<b)为升序 b-a(a>b)为降序)
Collections.sort(l,(a,b)->a-b);//升序
Collections.sort(l,(a,b)->b-a);//降序
PriorityQueue heap=new PriorityQueue<>((a,b)->a.weight-b.weight);

Collections.sort(mm,(a,b)->(b.height-a.height)<=0?-1:1); height为double 会容易超时
一般:
Collections.sort(mm,(a,b)->a-b);升序

这个不会超时

Collections.sort(mm, new Comparator<monbing>() {
    @Override
    public int compare(monbing o1, monbing o2) {
    //为降序
    if(o1.height > o2.height) return -1;
      else if(o1.height == o2.height) return 0;
    return 1;
   }
});
Collections.sort(ban,new Comparator<Ban>() {
				public int compare(Ban a,Ban b)
				{
					if(a.c==b.c)
					{
						return a.k<b.k?-1:1;
					}
					return a.c<b.c?-1:1;
				}
			});

复制容器:

方法1:

这个方法有缺点,当原容器变化时,c也会变化

LinkedList<Integer> a=new LinkedList<>();
LinkedList< Integer> c=a;

方法2:

最常用的,不会改变容器的方法

LinkedList< Integer> c=new LinkedList<>();
c.addAll(a);
System.out.println(c);

容器例题:

力扣5723:
在这里插入图片描述

import java.math.*;
class Solution {
    public int[] findingUsersActiveMinutes(int[][] logs, int k) {
        Map<Integer ,Set<Integer>> m=new HashMap<>();
        for(int i=0;i<logs.length;i++)
        {
            int id=logs[i][0];int t=logs[i][1];
            if(!m.containsKey(id)){
                Set<Integer> s=new HashSet<>();
                s.add(t);
                m.put(id,s);
            }
            else
            m.get(id).add(t);
        }
        int ans[]=new int[k];
        for(Map.Entry<Integer,Set<Integer>> en:m.entrySet())
        {
            ans[en.getValue().size()-1]++;
        }
        return ans;
    }
}

优先队列:实际上是一个堆,默认为小根堆,就是数据小的优先级高,所以无论按什么顺序add一堆树,在队列里都是先出最小的元素

PriorityQueue heap=new PriorityQueue<>((a,b)->a.weight-b.weight);

邻接矩阵:存所有点两两之间的关系
邻接表:存所有点的邻居
伪代码实现:

图的建立

1,邻接矩阵:

空间复杂度大,浪费空间,不容易遍历

int map[][]=new int[n][n]

2,邻接链表:

容易遍历,空间节约

无权值代码实现
ArrayList<Integer> a[]=new ArrayList[7];
		for(int i=0;i<7;i++)
		{
			a[i]=new ArrayList<>();
		}
		a[1].add(2);a[1].add(3);
		a[2].add(5);
		for(int i=0;i<a.length;i++)
		{
			for(int j=0;j<a[i].size();j++)
			{
				System.out.println(i+" "+a[i].get(j));
			}
		}

####带权值代码实现

3,前向星

前向星是一种特殊的边集数组,我们把边集数组中的每一条边按照起点从小到大排序,如果起点相同就按照终点从小到大排序,

并记录下以某个点为起点的所有边在数组中的起始位置和存储长度,那么前向星就构造好了.

用len[i]来记录所有以i为起点的边在数组中的存储长度.
用head[i]记录以i为边集在数组中的第一个存储位置.

那么对于下图:

在这里插入图片描述

我们输入边的顺序为:

1 2

2 3

3 4

1 3

4 1

1 5

4 5

那么排完序后就得到:

编号: 1 2 3 4 5 6 7

起点u: 1 1 1 2 3 4 4

终点v: 2 3 5 3 4 1 5

得到:

head[1] = 1 len[1] = 3

head[2] = 4 len[2] = 1

head[3] = 5 len[3] = 1

head[4] = 6 len[4] = 2

需要将起点从大到小进行排序。

4,链式前向星(常用)

在前向星的基础上,连接通道就不用排序,就成了链式前向星。
用链式前向星,就可以避免了排序.

实现:

给每一条边取号
e[i]第i边的终点
ne[i] 为上一条出边(值为边号开始都是,head的初始值,0或-1)
w[i] 第i边的权值
h[i] 结点为i的最后一条边

const int N=1e5+10,INF=0x3f3f3f3f;
int h[N],e[N],ne[N],idx,w[N];

void add(int a,int b,int c){
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
for(int i=h[t];i!=-1;i=ne[i])
{    
 }
cin>>n>>m;
while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }

java+结构体:
head[i] 存i结点最后一条出边
e[i]存第i条边
e[i].next为上一条出边(值为边号开始都是,head的初始值,0或-1)

从head[i]dao->e[i].next可以遍历一遍i结点的所有边。


#include<bits/stdc++.h>
const int maxx=100010;
using namespace std;
struct node
{
    int to;//表示第i条边的终点
    int next;//表示与第i条边同起点的下一条边的存储位置
    int w;//权值
} edge[maxx];
int head[maxx];
int cnt;
void add(int u,int v,int w)
{
    edge[cnt].w=w;
    edge[cnt].to=v;//表示第i条边的终点
    edge[cnt].next=head[u];//head[i]表示以i为起点的最后一条边的储存位置
    head[u]=cnt++;
}
int main()
{
    memset(head,0,sizeof(head));
    cnt=1;
    int n;
    scanf("%d",&n);
    int a,b,c;
    while(n--)
    {
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    int start;
    scanf("%d",&start);
    for(int i=head[start]; i!=0; i=edge[i].next)
        printf("%d->%d %d\n",start,edge[i].to,edge[i].w);
    return 0;
}
/*
6
1 2 3
2 3 5
3 4 6
1 3 8
4 1 9
1 5 6
*/

拓扑排序

用法:

拓扑排序是一个有向无环图(DAG)(有无权值都行)的所有结点的线性序列
是一个遍历图的很好序列。
可以在图上进行遍历,因为拓扑排序具有无后效性(前面的数据不会被后面的影响),dp也具有无后效性,所以可以轻松结合使用

条件

题目为有向无环图时就要想到用拓扑排序。
该序列满足两个条件:
1,每个顶点出现且只出现一次
2,在序列中,起点在终点前面。(可以不相邻)
注意:有向无环图才有拓扑排序。
如果没有字典序要求,一个有向无环图的拓扑排序可能会多种

实现操作:

1,先统计所有节点的入度(相邻结点到自身结点,则自身结点入度+1)
入度为0的节点就可以分离出来,再把这个节点指向的节点入度-1
2,重复操作,直到全部分离,就可以得到拓扑排序

在这里插入图片描述

模板:

1,创建邻接表
2,用优先队列存放入度为0的点,会弹出最小值的点
3,Ans存储 队列poll出的结点

import java.util.LinkedList;
//2021年3月21日下午4:26:25
//writer:apple
import java.util.Scanner;
import java.util.PriorityQueue;
public class tuopusort {
	static int n;
	static int m;
	static int indu[];
	static LinkedList<Integer> a[];//创建邻接表
	static PriorityQueue<Integer> ans=new PriorityQueue();//存放入度为0的结点
	static LinkedList<Integer> Ans=new LinkedList<>();
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner s=new Scanner(System.in);
		n=s.nextInt();m=s.nextInt();
		a=new LinkedList[n+1];
		indu=new int[n+1];
		for(int i=0;i<=n;i++)
		{
			a[i]=new LinkedList();
		}
		
		for(int i=0;i<m;i++)
		{
			int b=s.nextInt();
			int c=s.nextInt();
			a[b].add(c);
			indu[c]++;
		}
		for(int i=1;i<=n;i++)
		{
			if(indu[i]==0)
			{
				ans.add(i);
			}
		}
		while(!ans.isEmpty())
		{
			int x=ans.poll();
			Ans.add(x);//最后的答案
			//遍历结点的邻接点
			for(int i=0;i<a[x].size();i++)
			{
				int y=a[x].get(i);
				indu[y]--;
				if(indu[y]==0) ans.add(y);
			}			
		}
		for(int i:Ans)
			System.out.print(i+" ");
	}
}

例题:

1,P1137 旅行计划

这题比较简单,,在建立拓扑序列的同时,dp就行。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.PriorityQueue;
import java.util.Arrays;
import java.util.LinkedList;

//2021年3月22日上午12:58:41
//writer:apple
public class Main {

	static int n;
	static int m;
	static int indu[];
	static PriorityQueue<Integer> p=new PriorityQueue<>();
	static LinkedList<Integer> map[];
	static int dp[];
	public static void main(String[] args) throws IOException {
		// TODO Auto-generated method stub
		BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
		String t[]=br.readLine().split(" ");
		n=Integer.parseInt(t[0]);m=Integer.parseInt(t[1]);
		map=new LinkedList[n+1];
		indu=new int[n+1];
		dp=new int[n+1];
		Arrays.fill(dp, 1);
		for(int i=0;i<=n;i++) map[i]=new LinkedList<>();
		for(int i=0;i<m;i++)
		{
			String tt[]=br.readLine().split(" ");
			int a=Integer.parseInt(tt[0]);int b=Integer.parseInt(tt[1]);
			map[a].add(b);
			indu[b]++;
		}
		for(int i=1;i<=n;i++)
		{
			if(indu[i]==0) p.offer(i);
		}
		
		while(!p.isEmpty())
		{
			int x=p.poll();
			for(int i=0;i<map[x].size();i++)
			{
				int y=map[x].get(i);
				indu[y]--;
				if(indu[y]==0) p.offer(y);
				dp[y]=Math.max(dp[y],dp[x]+1);
			}
		}
		for(int i=1;i<=n;i++)
		{
			System.out.println(dp[i]);
		}
	}
}

2,P1807 最长路

用map存储点a,点b,和权值

package tu;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.PriorityQueue;

//2021年3月22日上午12:16:21
//writer:apple
public class tuopucccc {
	static int n;
	static int m;
	static int indu[];
	static PriorityQueue<Integer> p=new PriorityQueue<>();
	static boolean vis[];
	static LinkedList<Integer> ans=new LinkedList<>();//存放拓扑序列
	static int dp[];//dp[i]为1到i的最长路
	static class pair{
		int x;
		int weight;
		public pair(int xx,int w)
		{
			x=xx;weight=w;
		}
	}

	public static void main(String[] args) throws IOException {
		// TODO Auto-generated method stub
		BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
		Map<Integer,LinkedList<pair>> map=new HashMap<>();
		String t[]=br.readLine().split(" ");
		n=Integer.parseInt(t[0]);m=Integer.parseInt(t[1]);
		indu=new int[n+1];
		vis=new boolean[n+1];
		dp=new int[n+1];
		for(int i=1;i<=n;i++)
		{
			map.putIfAbsent(i, new LinkedList<>());
		}
		for(int i=0;i<m;i++)
		{
			String temp[]=br.readLine().split(" ");
			int a=Integer.parseInt(temp[0]);int b=Integer.parseInt(temp[1]);int len=Integer.parseInt(temp[2]);
			indu[b]++;
			map.putIfAbsent(a,new LinkedList<>());
			map.get(a).add(new pair(b,len));
		}
		for(int i=1;i<=n;i++)
		{
			if(indu[i]==0) p.add(i);
		}
		dp[n]=-1;
		vis[1]=true;
		while(!p.isEmpty()) {
			int x=p.poll();
			for(int i=0;i<map.get(x).size();i++)
			{
				int y=map.get(x).get(i).x;
				indu[y]--;
				if(indu[y]==0) p.offer(y);
				if(vis[x])
				{
					dp[y]=Math.max(dp[y],dp[x]+map.get(x).get(i).weight);
					vis[y]=true;
				}
			}
		}
		System.out.println(dp[n]);
	}

}


找环(保存路径):

bool dfs(int u){
	if(vis[u]){
		if(u == idx) return true;
		return false;
	}
	vis[u] = 1;
	for(int i = 0; i < g[u].size(); i++){
		int v = g[u][i];
		if( dfs(v) ){
			ans.push_back(v);
			return true;
		} 
	}
	return false;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值