邻接矩阵
逻辑结构分为两部分:V和E集合,其中,V是顶点,E是边。因此,用一个一维数组存放图中所有顶点数据;用一个二维数组存放顶点间关系(边或弧)的数据,这个二维数组称为邻接矩阵。邻接矩阵又分为有向图邻接矩阵和无向图邻接矩阵。
因此只要对任意一个图,都可以使用邻接矩阵表示。
邻接矩阵(Adjacency Matrix)是表示顶点之间相邻关系的矩阵。设G=(V,E)是一个图,其中V={v1,v2,…,vn} 。G的邻接矩阵是一个具有下列性质的n阶方阵:
1. 对无向图而言,邻接矩阵一定是对称的,而且主对角线一定为零(在此仅讨论无向简单图),副对角线不一定为0,有向图则不一定如此。
2. 在无向图中,任一顶点i的度为第i列(或第i行)所有非零元素的个数,在有向图中顶点i的出度为第i行所有非零元素的个数,而入度为第i列所有非零元素的个数。
3. 用邻接矩阵法表示图共需要n^2个空间,由于无向图的邻接矩阵一定具有对称关系,所以扣除对角线为零外,仅需要存储上三角形或下三角形的数据即可,因此仅需要n(n-1)/2个空间。
这样我们就可以把一个抽象的图,转化为程序可以识别的矩阵数学逻辑问题了。
深度优先遍历
深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
深度优先遍历图的方法是,从图中某顶点v出发:
(1)访问顶点v;
(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
深度优先遍历具有探索特性,可以优先寻找一切能够实现的路径。因此,连通性类的所有题目适用于深度优先遍历。
连通性判断
【问题描述】给定一个方阵,定义连通:上下左右相邻,并且值相同。
可以想象成一张地图,不同的区域被涂以不同颜色。
输入:
整数N, (N<50)表示矩阵的行列数
接下来N行,每行N个字符,代表方阵中的元素
接下来一个整数M,(M<1000)表示询问数
接下来M行,每行代表一个询问,
格式为4个整数,y1,x1,y2,x2,
表示(第y1行,第x1列) 与 (第y2行,第x2列) 是否连通。
连通输出true,否则false
例如:
10
0010000000
0011100000
0000111110
0001100010
1111010010
0000010010
0000010011
0111111000
0000010000
0000000000
3
0 0 9 9
0 2 6 8
4 4 4 6程序应该输出:
false
true
true
import java.util.Scanner;
public class Connectivity {
static boolean connect(char[][] data,int y1,int x1,int y2,int x2) {
if(y1 == y2 && x1 == x2)
return true;
char old = data[y1][x1];
data[y1][x1] = '*';
try {
if(y1 > 0 && data[y1 - 1][x1] == old && connect(data,y1 - 1,x1,y2,x2))
return true;
if(y1 < data.length-1 && data[y1 + 1][x1] == old && connect(data,y1 + 1,x1,y2,x2))
return true;
if(x1 > 0 && data[y1][x1 - 1] == old && connect(data,y1,x1 - 1,y2,x2))
return true;
if(x1 < data.length-1 && data[y1][x1 + 1] == old && connect(data,y1,x1 + 1,y2,x2))
return true;
}
finally{
data[y1][x1] = old;
}
return false;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = Integer.parseInt(sc.nextLine());
char[][] data = new char[N][];
for(int i=0; i<N; i++){
data[i] = sc.nextLine().toCharArray();
}
int M = Integer.parseInt(sc.nextLine());
for(int i=0; i<M; i++){
String[] ss = sc.nextLine().split(" ");
int y1 = Integer.parseInt(ss[0]);
int x1 = Integer.parseInt(ss[1]);
int y2 = Integer.parseInt(ss[2]);
int x2 = Integer.parseInt(ss[3]);
System.out.println(connect(data,y1,x1,y2,x2));
}
}
}
风险度量
【问题描述】
标题:风险度量
X星系的的防卫体系包含 n 个空间站。这 n 个空间站间有 m 条通信链路,构成通信网。
两个空间站间可能直接通信,也可能通过其它空间站中转。
对于两个站点x和y (x != y), 如果能找到一个站点z,使得:
当z被破坏后,x和y不连通,则称z为关于x,y的关键站点。
显然,对于给定的两个站点,关于它们的关键点的个数越多,通信风险越大。
你的任务是:已经网络结构,求两站点之间的通信风险度,即:它们之间的关键点的个数。
输入数据第一行包含2个整数n(2 <= n <= 1000), m(0 <= m <= 2000),分别代表站点数,链路数。
空间站的编号从1到n。通信链路用其两端的站点编号表示。
接下来m行,每行两个整数 u,v (1 <= u, v <= n; u != v)代表一条链路。
最后1行,两个数u,v,代表被询问通信风险度的两个站点。
输出:一个整数,如果询问的两点不连通则输出-1.
例如:
用户输入:
7 6
1 3
2 3
3 4
3 5
4 5
5 6
1 6
则程序应该输出:
2
import java.util.List;
import java.util.Scanner;
import java.util.Vector;
public class RiskMeasurement {
static void fan_zu(int[][] re, int me, int goal){
if(re[me][3] <= goal) return;
re[me][3] = goal;
fan_zu(re, re[me][2], goal);
}
static void dfs(List[] gr, int[][] re, int v1, int v2){
if(v1==v2) return;
for(Object obj: gr[v1]){
int it = (Integer)obj;
if(re[it][0] > 0){
fan_zu(re,v1,re[it][1]); // 更新整个家族的返祖级
continue;
}
re[it][0] = 1;
re[it][1] = re[v1][1]+1;
re[it][2] = v1;
re[it][3] = re[v1][1];
dfs(gr,re,it,v2);
}
}
static int solve(int[][] re, int root, int leaf){
int sum = 0;
int p = leaf;
int min = re[p][3]; // 当前最高(小)返祖级
while(true){
int pa = re[p][2];
System.out.println("pa " + pa);
if(pa==0 || pa==root) break;
if(re[pa][1] <= min) sum++;
if(re[pa][3] < min) min = re[pa][3];
p = pa;
}
return sum;
}
public static void main(String[] args)
{
Scanner scan = new Scanner(System.in);
String[] ss = scan.nextLine().trim().split(" ");
int m = Integer.parseInt(ss[0]); //节点数
int n = Integer.parseInt(ss[1]); //边数
List[] gr = new List[m+1]; // 顶点 --> list(顶点)
for(int i=0; i<gr.length; i++) gr[i] = new Vector();
int[][] re = new int[m+1][4]; // dfs生成树结果: 顶点 --> (可见性, 深度, 父节点, 最高返祖)
for(int i=0; i<n; i++){
ss = scan.nextLine().trim().split(" ");
int v1 = Integer.parseInt(ss[0]);
int v2 = Integer.parseInt(ss[1]);
gr[v1].add(v2);
gr[v2].add(v1);
}
ss = scan.nextLine().trim().split(" ");
int a = Integer.parseInt(ss[0]);
int b = Integer.parseInt(ss[1]);
re[a][0] = 1;
re[a][1] = 0;
re[1][3] = 0;
dfs(gr,re,a,b);
System.out.println(solve(re,a,b));
}
static void debug(int[][] re){
for(int i=0; i<re.length; i++){
System.out.println(i + ": " + re[i][0] + "," + re[i][1] + "," + re[i][2] + "," + re[i][3]);
}
}
}
广度优先遍历
宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
广度优先遍历方法类似于层序遍历,从一个点开始,所有路径同时开始遍历,像一个潮水,从开始到结尾,所有路径将被一次性遍历。
广度优先遍历具有扫描特性,可以同时探索所有路径,因此首先返回的一定是最短路径。因此,广度优先遍历广泛用于寻找最短路径算法和最少操作数的题目。
迷宫问题
【问题描述】
…11111111111111111111111111111
11.111111…1111111111.1111
11.111111…111.11111111…1111
11.11111111111.1111111111.111111
11.111111…111111
11.111111.11111111111.11111.1111
11.111111.11111111111.11111…111
11…111111111.11111.1111
11111.111111111111111.11…1111
11111.111111111111111.11.11.1111
11111.111111111111111.11.11.1111
111…111111111111111.11.11.1111
111.11111111111111111…11.1111
111.11111111111111111111111.1111
111.1111.111111111111111…11
111.1111…111111111.1111.11
111.1111.11111.111111111.1111.11
111…11111.111111111.1111111
11111111111111.111111111.111…1
11111111111111…1.1
111111111111111111111111111111…
如上图的迷宫,入口,出口分别:左上角,右下角
“1"是墙壁,”."是通路
求最短需要走多少步?
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class MazeProblem {
static int maze(char[][] data,Set come,String goal) {
if(come.contains(goal))
return 0;
Set set = new HashSet();
for(Object object : come) {
String[] crr = ((String)object).split(",");
int y = Integer.parseInt(crr[0]);
int x = Integer.parseInt(crr[1]);
if(y > 0 && data[y - 1][x] == '.') {
data[y - 1][x] = '*';
set.add(y-1 + "," + x);
}
if(y < data.length - 1 && data[y + 1][x] == '.') {
data[y + 1][x] = '*';
set.add(y+1 +","+x);
}
if(x > 0 && data[y][x - 1] == '.') {
data[y][x - 1] = '*';
set.add(y+","+(x - 1));
}
if(x < data[0].length - 1 && data[y][x + 1] == '.') {
data[y][x + 1] = '*';
set.add(y+","+(x + 1));
}
}
if(set.isEmpty())
return -1;
int r = maze(data,set,goal);
if(r < 0)
return r;
return r + 1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = Integer.parseInt(sc.nextLine());
int m = Integer.parseInt(sc.nextLine());
char[][] data = new char[n][];
for(int i = 0;i<data.length;i++) {
data[i] = sc.nextLine().trim().toCharArray();
}
Set set = new HashSet();
set.add("0,0");
System.out.println(maze(data,set,n-1 + "," + (m-1)));
}
}
分酒问题(韩信走马分油)
有4个红酒瓶子,它们的容量分别是:9升, 7升, 4升, 2升
开始的状态是 [9,0,0,0],也就是说:第一个瓶子满着,其它的都空着。允许把酒从一个瓶子倒入另一个瓶子,但只能把一个瓶子倒满或把一个瓶子倒空,不能有中间状态。
这样的一次倒酒动作称为1次操作。假设瓶子的容量和初始状态不变,对于给定的目标状态,至少需要多少次操作才能实现?
本题就是要求你编程实现最小操作次数的计算。输入:最终状态(空格分隔)
输出:最小操作次数(如无法实现,则输出-1)例如:
输入:
9 0 0 0
应该输出:
0输入:
6 0 0 3
应该输出:
-1输入:
7 2 0 0
应该输出:
2
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class LiquorDistribution {
static Set dispose(String liquor) {
int[] bottle = {9,7,4,2};//瓶子的容积
int[] data = new int[4];
String[] ss = liquor.split(" ");
for(int i = 0;i<4;i++)
data[i] = Integer.parseInt(ss[i]);
Set set = new HashSet();
for(int i = 0;i<4;i++) {//以i所在的瓶子为酒
for(int j = 0;j<4;j++) {//以j所在瓶子为目标,将i向j倒入
if(i == j)
continue;
if(data[i] == 0)
continue;
if(data[j] == bottle[j])
continue;
int sum = data[i] + data[j];
int Vi = (sum <= bottle[j]) ? 0 : (sum - bottle[j]);//i所在的酒的瓶子
int Vj = (sum <= bottle[j]) ? sum : bottle[j];//j所在的目标的的瓶子
String result = "";
for(int k = 0;k<4;k++) {
if(k == i)
result += Vi + " ";
else if(k == j)
result += Vj + " ";
else
result += data[k] + " ";
}
set.add(result.trim());
}
}
return set;
}
static int distribute(Set history,Set begin,String end) {
if(begin.contains(end))
return 0;
Set set = new HashSet();
for(Object obj:begin) {
Set temp = dispose(obj.toString());
set.addAll(temp);
}
set.removeAll(history);
if(set.isEmpty())
return -1;
history.addAll(set);
int r = distribute(history,set,end);
if(r < 0)
return r;
return r + 1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Set begin = new HashSet();
begin.add("9 0 0 0");
Set history = new HashSet();
history.addAll(begin);
System.out.println(distribute(history,begin,sc.nextLine().trim()));
}
}
青蛙交换问题
【问题描述】
X星球的流行宠物是青蛙,一般有两种颜色:白色和黑色。
X星球的居民喜欢把它们放在一排茶杯里,这样可以观察它们跳来跳去。
如下图,有一排杯子,左边的一个是空着的,右边的杯子,每个里边有一只青蛙。
*WWWBBB
其中,W字母表示白色青蛙,B表示黑色青蛙,*表示空杯子。
X星的青蛙很有些癖好,它们只做3个动作之一:
- 跳到相邻的空杯子里。
- 隔着1只其它的青蛙(随便什么颜色)跳到空杯子里。
- 隔着2只其它的青蛙(随便什么颜色)跳到空杯子里。
对于上图的局面,只要1步,就可跳成下图局面:
WWW · BBB
本题的任务就是已知初始局面,询问至少需要几步,才能跳成另一个目标局面。
输入为2行,2个串,表示初始局面和目标局面。
输出要求为一个整数,表示至少需要多少步的青蛙跳。
例如:
输入:
· WWBB
WWBB ·
则程序应该输出:
2
再例如,
输入:
WWW · BBB
BBB · WWW
则程序应该输出:
10
我们约定,输入的串的长度不超过15
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class frogJump {
static void move(char[] arr,int i,int step,Set frogs) {
if(arr[i] == '*')
return;
int moved = i + step;
if(moved < 0 || moved >= arr.length)
return;
if(arr[moved] != '*')
return;
arr[moved] = arr[i];
arr[i] = '*';
frogs.add(new String(arr));
arr[i] = arr[moved];
arr[moved] = '*';
}
static Set Frog(String frog) {
Set s = new HashSet();
char[] arr = frog.toCharArray();
for(int i = 0;i<arr.length;i++) {
move(arr,i,-1,s);
move(arr,i,-2,s);
move(arr,i,-3,s);
move(arr,i,1,s);
move(arr,i,2,s);
move(arr,i,3,s);
}
return s;
}
static int Jump(Set begin,Set history,String goal) {
if(begin.contains(goal))
return 0;
Set set = new HashSet();
for(Object obj:begin) {
Set temp = Frog(obj.toString());
set.addAll(temp);
}
set.removeAll(history);
if(set.isEmpty())
return -1;
history.addAll(set);
int r = Jump(set,history,goal);
if(r < 0)
return r;
return r+1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Set begin = new HashSet();
begin.add(sc.nextLine().trim());
Set history = new HashSet();
history.addAll(begin);
System.out.println(Jump(begin,history,sc.nextLine().trim()));
}
}