1.图的遍历
1.生化危机
可用dfs或者bfs遍历即可:
#include <bits/stdc++.h>
using namespace std;
int main(){
int m,n,k;
cin>>m>>n>>k;
map<int,vector<int>> st;
for (int i = 0; i <m ; ++i) {
st[i]=vector<int>{};
}
//安全城市
vector<bool> isSafe(m, false);
vector<bool> vis(m, false);
for (int i = 0; i < n; ++i) {
int temp;
cin>>temp;
isSafe[temp]= true;
}
//公路
int a,b;
for (int j = 0; j <k ; ++j) {
cin>>a>>b;
st[a].push_back(b);
st[b].push_back(a);
}
//起止
int start,end;
cin>>start>>end;
if(!isSafe[end]){
cout<<"The city "<<end<<" is not safe!"<<endl;
return 0;
}
//队列
queue<int> q;
q.push(start);
while(!q.empty()){
int top=q.front();
q.pop();
vis[top]= true;
if(top==end){
cout<<"The city "<<end<<" can arrive safely!"<<endl;
return 0;
}
for (int i = 0; i < st[top].size() ; ++i) {
int cur=st[top][i];
if(!vis[cur]&& isSafe[cur]){
q.push(cur);
}
}
}
cout<<"The city "<<end<<" can not arrive safely!";
}
2.节点间通路
考察图的遍历:
//bfs版本:
bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
unordered_map<int,vector<int>> st;
vector<bool> vis(n+1, false);
for( auto k: graph){
st[k[0]].push_back(k[1]);
}
queue<int> qu;
qu.push(start);
while ( !qu.empty() ){
int top=qu.front();
qu.pop();
vis[top]= true;
if (top == target )return true;
for (int i=0;i<st[top].size();i++){
if( !vis[st[top][i]]){
qu.push(st[top][i]);
}
}
}
return false;
}
//dfs版本:
map<int,vector<int>> st;
vector<bool > flag;
bool dfs(int cur,int target){
if(cur==target){
return true;
}
flag[cur]= true;
for(int i=0;i<st[cur].size();i++){
if(!flag[st[cur][i]]){
if(dfs(st[cur][i],target))return true;
}
}
return false;
}
bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
flag=vector<bool>(n, false);
for (int i = 0; i <n ; ++i) {
st[i]=vector<int>();
}
for(auto k:graph){
st[k[0]].push_back(k[1]);
}
return dfs(start,target);
}
2.最短路
1. 哈利·波特的考试
输入样例
6 11
3 4 70
1 2 1
5 4 50
2 6 50
5 6 60
1 3 70
4 6 60
3 6 80
5 1 100
2 4 60
5 2 80
输出样例:
4 70
题目分析:
简单理解就是:
每个节点到其他节点的最短的路径中,最长的那一条构建一个集合
从这个集合中寻找最短的一跳,作为答案
这里用的是floyd最短路算法
#include <bits/stdc++.h>
using namespace std;
int inf=1000000;
//寻找节点V到其他节点的最长路径
int find(vector<vector<int>> graph,int v){
int m=-100000;
for (int i = 1; i <graph.size() ; ++i) {
if( i!= v){
m=max(m,graph[v][i]);
}
}
return m;
}
int main(){
int n,m;
cin>>n>>m;
vector<vector<int>> graph(n+1,vector<int>(n+1,inf));
int a,b,value;
for (int i = 0; i <m ; ++i) {
cin>>a>>b>>value;
graph[a][b]=value;
graph[b][a]=value;
}
#floyd构建最短路
for (int k = 1; k <=n ; ++k) {
for (int i = 1; i <=n ; ++i) {
for (int j = 1; j <=n ; ++j) {
graph[i][j]=min(graph[i][j],graph[i][k]+graph[k][j]);
}
}
}
int ans=0;
int maxLen=inf;
#寻找答案
for(int i=1;i<=n;i++){
int cur=find(graph,i);
if(cur<maxLen){
maxLen=cur;
ans=i;
}
}
if( maxLen != inf){
cout<<ans<<" "<<maxLen;
}else{
cout<<0;
}
}
2. 被Gank的亚索
输入示例
5 6 1 5
1 2 5
2 5 5
1 3 3
3 5 7
1 4 1
4 5 9
输出示例
10 3
题目分析:
题目想让我们找最短路径的数目,需要注意的是,两个节点间读取数据时相同长度的路径不止一条(想一想LOL的地图),因此应该事先记录
其次,因为两节点间的路径不止一条,所以在计算答案的时候,应用*,而不是+
堆优化版dijkstra最短路:
#include <bits/stdc++.h>
using namespace std;
int main(){
int n,m,s,e;
cin>>n>>m>>s>>e;
int inf=0x3f3f3f3f;
typedef pair<int,int> ll;
//图
vector<vector<int>> graph(n+1,vector<int>(n+1,0));
//堆
priority_queue<ll,vector<ll>,greater<ll>> st;
//距离
vector<int> dist(n+1,inf);
//标志
vector<bool> vis(n+1, false);
//路径数量
vector<int> lowestNums(n+1,1);
vector<vector<int>> roadNums(n+1,vector<int>(n+1,0));
int a,b,value;
for (int i = 0; i <m ; ++i) {
cin>>a>>b>>value;
if ( ! graph[a][b] || value<graph[a][b] ){
graph[a][b]=value;
graph[b][a]=value;
roadNums[a][b]=1;
roadNums[b][a]=1;
}else if ( value== graph[a][b] ){
roadNums[a][b]++;
roadNums[b][a]++;
}
}
st.push({0,s});
dist[s]=0;
vis[s]= true;
while ( !st.empty() ){
ll cur= st.top();
st.pop();
vis[cur.second]= true;
for( int i=1;i<=n;i++){
if( graph[cur.second][i] && !vis[i] ){
if( cur.first+graph[cur.second][i] <dist[i] ){
dist[i]= cur.first+graph[cur.second][i];
st.push({dist[i],i});
lowestNums[i] = lowestNums[cur.second]*roadNums[cur.second][i];
}
else if ( cur.first+graph[cur.second][i] == dist[i] ){
lowestNums[i] += lowestNums[cur.second]*roadNums[cur.second][i];
}
}
}
}
cout<< dist[e]<<" "<<lowestNums[e];
}
3.并查集
1.合并账户
并查集,问题在于,如何记录两个列表是否是同一个人的?
判断依据:有没有相同的邮箱
所以可以这样想
初始版本:
1.循环每一个列表的每一个邮箱,和别的列表比较,是否存在别的列表中
改进:
1.用一个set保存邮箱,如果邮箱没有出现过,就记录第一次出现时的父类
2.如果出现过,就合并两个父类
c++;
class Solution {
public:
int graph[10000]={0};
int find(int i){
if(graph[i]==i)return graph[i];
else return graph[i]=find(graph[i]);
}
void Union(int i,int j){
int p1=find(i);
int p2=find(j);
if(p1!=p2){
graph[p2]=p1;
}
}
//用于保存该邮箱是否有出现过
set<string>isTurn;
//用于保存该邮箱的父类
map<string,int> emailFa;
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
int n=accounts.size();
for(int i=0;i<n;i++){
graph[i]=i;
}
//1.遍历,合并
for(int i=0;i<n;i++){
for(int j=1;j<accounts[i].size();j++){
if(isTurn.count(accounts[i][j])==0){
//第一次出现,则保存记录
isTurn.insert(accounts[i][j]);
emailFa[accounts[i][j]]=i;
}else{
//否则,说明同一个人,合并
Union(emailFa[accounts[i][j]],i);
}
}
}
//2.将每个邮箱放到相对应的父类下面,sort可以自动排序
map<int,set<string>> m;
for(int i=0;i<n;i++){
// cout<<graph[i]<<" ";
int p=find(i);
if(m.count(p)==0)m[p]=set<string>();
for(int j=1;j<accounts[i].size();j++){
m[p].insert(accounts[i][j]);
}
}
//转化成相应的结构
vector<vector<string>> result;
for(auto acc:m){
vector<string> ans;
ans.push_back(accounts[acc.first][0]);
for(auto s:acc.second){
ans.push_back(s);
}
result.push_back(ans);
}
return result;
}
};
2.移出最多的同行或同列石头
并查集,关键要找出什么和什么并起来
这一题很明显是点和点之间并(条件是两个点的行数或列数相同),并且在合并过程中求出每个簇下点的数量,
最后结果就是每个簇的点的数量-1,相加起来
class Solution {
public:
int graph[10000]={0};
int size[10000]={0};
int find(int i){
if(graph[i]==i)return graph[i];
else return graph[i]=find(graph[i]);
}
void Union(int i,int j){
int p1=find(i);
int p2=find(j);
if(p1!=p2){
if(size[p1]>size[p2]){
graph[p2]=p1;
size[p1]+=size[p2];
size[p2]=0;
}else{
graph[p1]=p2;
size[p2]+=size[p1];
size[p1]=0;
}
}
}
int removeStones(vector<vector<int>>& stones) {
int n=stones.size();
for(int i=0;i<n;i++){
graph[i]=i;
size[i]=1;
}
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(i!=j){
if(stones[i][0]==stones[j][0]||stones[i][1]==stones[j][1]){
Union(i,j);
}
}
}
}
int cnt=0;
for(int i=0;i<n;i++){
//cout<<size[i]<<" ";
if(size[i]>0)cnt+=size[i]-1;
}
return cnt;
}
};
3.冗余连接
这个问题意思是,现在有一个无向且有还的图,如何删掉一条边,使其成为无环图
---->也即,并查集合并的时候,如果发现有祖先使相等的,就说明该条边造成了环,则记录该边
class Solution {
public:
int graph[10000]={0};
int size[10000]={0};
vector<int>result;
int find(int i){
if(graph[i]==i)return graph[i];
else return graph[i]=find(graph[i]);
}
void Union(int i,int j){
int p1=find(i);
int p2=find(j);
if(p1!=p2){
graph[p2]=p1;
}else{
//说明发生了冲突,则把该边加入结果集
result.push_back(i);
result.push_back(j);
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n=edges.size();
for(int i=0;i<=n;i++){
graph[i]=i;
}
for(int i=0;i<n;i++){
int l1=edges[i][0];
int l2=edges[i][1];
Union(l1,l2);
}
vector<int> v;
v.push_back(result[result.size()-2]);
v.push_back(result[result.size()-1]);
return v;
}
};
4.被围绕的区域
如何运用并查集?
1.将二维转化为一维,即i*row+j
2.创造虚拟节点row*col,将边界的和虚拟节点并,若非边界,就和上下左右并即可
int graph[10000]={0};
int find(int i){
if(graph[i]==i)return graph[i];
else return graph[i]=find(graph[i]);
}
int cnt=0;
void Union(int i,int j){
int p1=find(i);
int p2=find(j);
if(p1!=p2){
graph[p2]=p1;
}else{
//说明改变无需存在就已经联通,让可重新操作的次数++;
cnt++;
}
}
bool connected(int i,int j){
return find(i)==find(j);
}
//可以将二维转化成一维
int turnNode(int i,int j,int cow){
return i*cow+j;
}
//初始化函数
void init(int n){
for(int i=0;i<n;i++)graph[i]=i;
}
int direction[4][2]={1,0,-1,0,0,1,0,-1};
void solve(vector<vector<char>>& board) {
int row=board.size();
if(row==0)return;
int col=board[0].size();
init(row*col+1);
//创建一个虚拟节点,用于连接边界的O
int virtualNode=row*col;
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(board[i][j]=='O'){
if(i==0||i==row-1||j==0||j==col-1){
//如果是边界的一个点,就和虚拟节点并
Union(virtualNode,turnNode(i,j,row));
}else{
//否则就和上下左右并
for(auto k:direction){
int lx=i+k[0];
int ly=j+k[1];
if(lx>=0&&lx<row&&ly>=0&&ly<col&&board[lx][ly]=='O'){
Union(turnNode(i,j,row),turnNode(lx,ly,row));
}
}
}
}
}
}
//填充,如果父类为虚拟node,则不变,否则为x
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
cout<<graph[turnNode(i,j,row)]<<" ";
if(!connected(virtualNode,turnNode(i,j,row))){
board[i][j]='X';
}
}
cout<<endl;
}
}
5.统计参与通信的服务器
并查集,先算每一行,每一列,第一次出现的1的下标
接着,后面的电脑只需要并到前面记录的第一次出现的父类即可
用size代表该簇的大小
class Solution {
public:
int graph[1000000]={0};
int size[1000000]={0};
//查找
int find(int i){
if(graph[i]==i)return graph[i];
else return graph[i]=find(graph[i]);
}
//合并
void Union(int i,int j){
int p1=find(i);
int p2=find(j);
if(p1!=p2){
graph[p2]=p1;
size[p1]+=size[p2];
size[p2]=0;
}
}
bool connected(int i,int j){
return find(i)==find(j);
}
//可以将二维转化成一维
int node(int i,int j,int cow){
return i*cow+j;
}
//初始化函数
void init(int n){
for(int i=0;i<n;i++){
graph[i]=i;
size[i]=1;
}
}
int countServers(vector<vector<int>>& grid) {
int rows=grid.size();
int cols=grid[0].size();
// cout<<rows<<" "<<cols;
init(rows*cols);
map<int,int> row;
map<int,int>col;
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if(grid[i][j]){
//记录每个行,列第一次出现的位置
if(row.count(i)==0)row[i]=j;
if(col.count(j)==0)col[j]=i;
}
}
}
//合并
int s=grid[0].size();
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if(grid[i][j]){
if(row.count(i)){
Union(node(i,row[i],s),node(i,j,s));
}
if(col.count(j)){
Union(node(col[j],j,s),node(i,j,s));
}
}
}
}
int cnt=0;
for(int i=0;i<rows*cols;i++){
cout<<size[i]<<" ";
if(size[i]>1)cnt+=size[i];
}
return cnt;
}
};
6.保证图可以遍历
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fnz7uDax-1606570817500)(D:\Typora\算法和数据结构\图算法\assets\1599381490231.png)]
图中有三种类型的边
对于Alice而言,她可以走类型一和1和3,
Bob可以走2和3
因此,我们可以先将类型为3的边合并,
再在3合并后的基础上,分别合并1和2,也即,分别构建属于Bob和Alice的并查集,
如果在合并过程中,发现两个点无法合并,就说明这条边可以删除
class Solution {
public:
int getRoot(vector<int>& par,int x){
int root = x;
while(par[root]!=root){
root = par[root];
}
while(par[x]!=root){
int tmp = par[x];
par[x] = root;
x = tmp;
}
return root;
}
bool merge(vector<int>& par,int x,int y){
int _x = getRoot(par,x);
int _y = getRoot(par,y);
if(_x!=_y){
par[_x]=_y;
return true;
}
return false;
}
int maxNumEdgesToRemove(int n, vector<vector<int>>& edges) {
vector<int>par1 = vector<int>(n+1,0);
vector<int>par2;
int ans = 0;
int cnt1 = n,cnt2;
for(int i =1;i<=n;i++){
par1[i] = i;
}
//先添加第三种类型的边
for(int i = 0;i<edges.size();i++){
if(edges[i][0]==3){
if(!merge(par1,edges[i][1],edges[i][2]))
ans++;
else
cnt1--;
}
}
par2 = par1;
cnt2 = cnt1;
//再添加其余两种类型的边
for(int i = 0;i<edges.size();i++){
if(edges[i][0]==1){
if(!merge(par1,edges[i][1],edges[i][2]))
ans++;
else
cnt1--;
}else if(edges[i][0]==2){
if(!merge(par2,edges[i][1],edges[i][2]))
ans++;
else
cnt2--;
}
}
//如果连通集不为1,就说明不能完全遍历
if(cnt1!=1||cnt2!=1)
return -1;
return ans;
}
};
4.拓扑排序
1.课程表
模板题:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
//代表入度
vector<int> in(numCourses,0);
//邻接表
vector<vector<int>> st(numCourses,vector<int>{});
for (int i = 0; i <prerequisites.size() ; ++i) {
in[prerequisites[i][0]]++;
st[prerequisites[i][1]].push_back(prerequisites[i][0]);
}
queue<int> q;
for (int j = 0; j <numCourses ; ++j) {
if(!in[j])q.push(j);
}
while(!q.empty()){
int top=q.front();
q.pop();
for (int i = 0; i <st[top].size() ; ++i) {
in[st[top][i]]--;
if(!in[st[top][i]]){
q.push(st[top][i]);
}
}
}
for (int k = 0; k <numCourses ; ++k) {
if(in[k])return false;
}
return true;
}
};
2.矩阵中的最长递增路径
这是一道动态规划的题目,但用拓扑排序更方便
要求最长递增路径,比如从最有下角的两个1,
1->8
1->{6,2}
其实这里的1相当于第一层
接着2,6,8就是第二层
…
…
所以,最长的路径其实就是算层数,
即,每次拓扑排序遍历的是一整层的数据,而不是一个个遍历
class Solution {
public int longestIncreasingPath(int[][] matrix) {
if (matrix==null||matrix.length==0){
return 0;
}
int[][] count = new int[matrix.length][matrix[0].length];
//统计每个点的入度用count数组保存,因为是递增,所以如果在上下左右,每发现一个比当前点小的数,当前点入度+1
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
for (int[] d : direction) {
if (longestIncreasingPathVerify(matrix, i + d[0], j + d[1]) && matrix[i + d[0]][j + d[1]] < matrix[i][j]) {
count[i][j]++;
}
}
}
}
Deque<int[]> deque = new LinkedList<>();
//count数组中所有入度为0的点加入队列
for (int i = 0; i < count.length; i++) {
for (int j = 0; j < count[i].length; j++) {
if (count[i][j] == 0) {
deque.add(new int[]{i, j});
}
}
}
int ans = 0;
while (!deque.isEmpty()) {
ans++;
//这个跟课程表I那个题不一样,需要一层一层的出列,而不是一个一个的出,因为课程表那个不关心队列长度
for (int size = deque.size(); size > 0; size--) {
int[] poll = deque.poll();
for (int[] d : direction) {
if (longestIncreasingPathVerify(matrix, poll[0] + d[0], poll[1] + d[1]) && matrix[poll[0] + d[0]][poll[1] + d[1]] > matrix[poll[0]][poll[1]]) {
if (--count[poll[0] + d[0]][poll[1] + d[1]] == 0) {
deque.add(new int[]{poll[0] + d[0], poll[1] + d[1]});
}
}
}
}
}
return ans;
}
private boolean longestIncreasingPathVerify(int[][] matrix, int i, int j) {
return i >= 0 && j >= 0 && i < matrix.length && j < matrix[i].length;
}
}
5.最小生成树
1.公路村村通
输入样例:
6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3
输出:
10
题目分析
就是根据所有的边,构建一颗最小生成树,使所需费用最低,这里用的是Kruskal 算法:
#include <bits/stdc++.h>
using namespace std;
//结构Edge 代表边
struct Edge{
int a,b,w;
Edge(int a,int b ,int w):a(a),b(b),w(w){}
bool operator < (Edge o)const{
return w<o.w;
}
};
vector<Edge> g;
vector<int> fa;
//并查集路径压缩查找
int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
}
//kruska算法
int kruska(int n){
sort(g.begin(),g.end());
int ans=0;
int cnt=n;
for(Edge e:g){
int fax= find(e.a);
int fay= find(e.b);
if(fax!=fay){
cnt--;
fa[fax]=fay;
ans+=e.w;
}
}
return cnt==1?ans:-1;
}
int main(){
int n,m;
cin>>n>>m;
int a,b,value;
fa=vector<int>(n+1,0);
for (int i = 1; i <=n ; ++i) {
fa[i]=i;
}
for(int i=0;i<m;i++){
cin>>a>>b>>value;
g.push_back(Edge(a,b,value));
}
cout<<kruskal(n);
}