图论知识是算法当中的一个非常重要的模块,同时也可以算是对算法的学习正式进入入门的阶段。有关于图论的基础知识在这里就不做过多的介绍了,这一部分的知识详见树与二叉树和图这两篇以数据结构为基础的文章。本篇博客的题目来源全部来源于团体程序设计天梯赛-练习集中,主要是以模板为主,为今后做题做好准备。
Part 1 并查集
并查集可以算是图上的最常见的搜索算法,通俗来讲,并查集可以解决一些“溯源”的问题,比如说有关于连通图的问题,就可以拿并查集来解决。并查集的套路也非常固定,而且非常的实用。下面的题目都是基于并查集来做的(当然这些题目的解法不唯一,在这里只是展现用并查集处理问题的方法)。
//并查集模板
void init()//初始化
{
for (int i=1;i<=n;i++)
pre[i]=i;
}
int find(int x)//溯源
{
return pre[x]==x?x:find(pre[x]);//这一部分也可以写成非递归的形式
}
void union(int x,int y)//合并
{
x=find(x);
y=find(y);
if (x!=y)
pre[y]=x;
}
L2-010 排座位
这道题是一个非常经典的并查集模型题,“朋友的朋友就是朋友”就已经得到一个信息:基于“朋友”建图,我们可以根据朋友的关系去建立这一张关系网,进而根据不同的人物关系得到相应的结果,具体代码如下:
#include <bits/stdc++.h>
using namespace std;
int pre[105];
int g[105][105];
int n,m,q;
void init()
{
for (int i=1;i<=n;i++)
pre[i]=i;
}
int find(int x)
{
return pre[x]==x?x:find(pre[x]);
}
void union(int x,int y)
{
x=find(x);
y=find(y);
if (x!=y)
pre[y]=x;
}
int main()
{
cin>>n>>m>>q;
init();
while (m--)
{
int a,b,f;
cin>>a>>b>>f;
g[a][b]=g[b][a]=f;
if (f==1)
union(a,b);
}
int x1,y1;
while (q--)
{
int x,y;
cin>>x>>y;
x1=find(x);
y1=find(y);
if (g[x][y]!=-1&&x1==y1)
cout<<"No problem"<<endl;
else if (g[x][y]==0)
cout<<"OK"<<endl;
else if (g[x][y]==-1&&x1==y1)
cout<<"OK but..."<<endl;
else if (g[x][y]==-1&&x1!=y1)
cout<<"No way"<<endl;
}
return 0;
}
类似有关并查集的题目还有L2-013 红色警报、L2-024 部落等。
Part 2 树上的操作
树的操作相信大家有了数据结构的基础后应该有所了解了,但实际上我们在写代码的时候,其实是不会采用数据结构中的那种写法,而是用一种更加直观的方式去建树,并且去访问树中的节点(四种遍历)。
//建立一棵二叉树
struct node
{
int data;
node *l, *r;
};
int N, pre[maxn], in[maxn], post[maxn];//存储节点数组
node* create(int postL,int postR,int inL,int inR)
{
if(postL>postR)
return NULL;
node* root=new node;
root->data=post[postR];//先序这里等式右端为pre[preL]
int k;
for (k=inL;k<=inR;k++)
if (in[k]==post[postR])
break;
int numLeft=k-inL;
root->l=create(postL,postL+numLeft-1,inL,k-1);
root->r=create(postL+numLeft,postR-1,k+1,inR);
return root;
}
//层次遍历二叉树
void Bfs(node* root)
{
queue<node*> q;
q.push(root);
while(!q.empty())
{
node* cur=q.front();
q.pop();
cout<<cur->data<<" ";
if(cur->l!=NULL)
q.push(cur->l);
if(cur->r!=NULL)
q.push(cur->r);
}
}
L2-006 树的遍历
建树相信大家都不陌生,这里建树的过程和上面的代码一致,采用数组存储相应的顺序,最后按照一定的递归规则进行建树,层次遍历时采用BFS思想,利用一个队列进行存储,具体代码如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn=35;
struct node
{
int data;
node *l, *r;
};
int N, pre[maxn], in[maxn], post[maxn];//存储节点数组
node* create(int postL,int postR,int inL,int inR)
{
if(postL>postR)
return NULL;
node* root=new node;
root->data=post[postR];
int k;
for (k=inL;k<=inR;k++)
if (in[k]==post[postR])
break;
int numLeft=k-inL;
root->l=create(postL,postL+numLeft-1,inL,k-1);
root->r=create(postL+numLeft,postR-1,k+1,inR);
return root;
}
int indx = 1;
void Bfs(node* root)
{
queue<node*> q;
q.push(root);
while (!q.empty())
{
node* cur=q.front();
q.pop();
if (indx++!=1)
cout<<' '<<cur->data;
else
cout<<cur->data;
if(cur->l!=NULL)
q.push(cur->l);
if(cur->r!=NULL)
q.push(cur->r);
}
}
int main()
{
cin>>N;
for (int i=0;i<N;i++)
cin>>post[i];
for (int i=0;i<N;i++)
cin>>in[i];
node* root=create(0,N-1,0,N-1);
Bfs(root);
return 0;
}
类似的题目还有L2-004 这是二叉搜索树吗? 、L2-011 玩转二叉树等。
Part 3 最短路径Dijkstra
迪杰斯特拉算法是求最短路径中最常用的一种算法,具体该算法的推理过程在这里就不详细说明了,不同于数据结构中的写法,在这里采用了一种比较简洁的形式。
void dijkstra()
{
memset(vis,0,sizeof(vis));
for (int i=0;i<n;i++)
dist[i]=G[s][i];
int Min=inf;
int v=-1;
for (int i=0;i<n;i++)
{
Min=inf;
v=-1;
for (int j=0;j<n;j++)
{
if (!vis[j]&&dist[j]<Min)
{
Min=dist[j];
v=j;
}
}
vis[v]=1;
for (int j=0;j<n;j++)
{
if(!vis[j]&&dist[j]>dist[v]+G[v][j])
dist[j]=dist[v]+G[v][j];
}
}
}
L2-001 紧急救援
这是一道非常经典的最短路径算法问题,本题还要求输出路径,所以最后还要用DFS进行深度优先搜索,将答案记录在容器中一并输出。具体代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int maxn = 550;
int n,m,s,d,Maxv,cnt;
int val[maxn],vis[maxn],dist[maxn];
int G[maxn][maxn];
vector<int> ans,pre;
void dijkstra()
{
memset(vis,0,sizeof(vis));
for (int i=0;i<n;i++)
dist[i]=G[s][i];
int Min=inf;
int v=-1;
for (int i=0;i<n;i++)
{
Min=inf;
v=-1;
for (int j=0;j<n;j++)
{
if (!vis[j]&&dist[j]<Min)
{
Min=dist[j];
v=j;
}
}
vis[v]=1;
for (int j=0;j<n;j++)
{
if(!vis[j]&&dist[j]>dist[v]+G[v][j])
dist[j]=dist[v]+G[v][j];
}
}
}
void dfs(int x, int w)
{
if(x == d)
{
if (w>Maxv)
{
Maxv=w;
ans=pre;
}
cnt++;
return;
}
for (int i=0;i<n;i++)
{
if(G[x][i]&&!vis[i]&&dist[i]==dist[x]+G[x][i])
{
vis[i]=1;
pre.push_back(i);
dfs(i,w+val[i]);
pre.pop_back();
vis[i]=0;
}
}
}
int main()
{
int u,v,w;
cin>>n>>m>>s>>d;
for (int i=0;i<n;i++)
cin>>val[i];
for (int i=0;i<n;i++)
{
for (int j=0;j<n;j++)
{
if (i!=j)
G[i][j] = inf;
else
G[i][j] = 0;
}
}
for (int i=0;i<m;i++)
{
cin>>u>>v>>w;
G[u][v]=w;
G[v][u]=w;
}
dijkstra();
Maxv=0;
cnt=0;
memset(vis,0,sizeof(vis));
pre.push_back(s);
dfs(s,val[s]);
cout<<cnt<<" "<<Maxv<<endl;
int len=ans.size();
for (int i=0;i<len;i++)
{
if (i==0)
cout<<ans[i];
else
cout<<" "<<ans[i];
}
return 0;
}
Part 4 DFS与BFS
DFS与BFS都是图上非常重要的搜索算法,其中DFS应用较广泛,在基础的算法题目中经常见到(其实是本菜鸟确实不大会BFS),其基本的思想是回溯法,根据不同的题目,不同的要求去写相应的DFS函数,这里给出一个非常基础的DFS模板:
L2-020 功夫传人
这道题其实并没有利用到回溯思想,但确实是一道非常好的dfs裸题,每次递归的参数都有所变化,利用了一个二维vector记录变量,具体代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,k,x,vis[100010];
double sum,z,r;
vector<vector<int>>v;
void dfs(int index,double power)
{
if(vis[index])
{
sum+=power*v[index][0];
return ;
}
for(int i=0;i<v[index].size();i++)
dfs(v[index][i],power*(1-r/100));
}
int main()
{
cin>>n>>z>>r;
v.resize(n);
for(int i=0;i<n;i++)
{
cin>>k;
if(!k)
{
cin>>x;
vis[i]=1;
v[i].push_back(x);
}
while(k--)
{
cin>>x;
v[i].push_back(x);
}
}
dfs(0,z);
cout<<(int)sum;
}
L2-038 病毒溯源
光看标题就知道这是一个深度优先搜索的题目,在众多输入中找到一条最长的变异链。所以很明显,有关DFS算法的题目基本上都是要求最长的…、或者输出某一条路径等等这类关键字。和第一部分说明的并查集类似,并查集是一个建图的过程,这个是一个遍历的过程,所以如果发现这二者结合起来的时候也不必惊讶。具体代码如下:
#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<set>
#include<stack>
#include<string>
#include<queue>
#include<vector>
#include<cctype>
#include<map>
using namespace std;
int n;
const int maxn=10001;
vector<int>v[maxn];
vector<int>temp;
int t[maxn];
void Dfs(int index,vector<int>&p){//这里需要用到&p,否则会内存过大
if(p.size()>temp.size()){//找到更深的,更新temp数组
temp.clear();
temp=p;
}
for(int i=0;i<v[index].size();i++){//标号为index的孩子节点
p.push_back(v[index][i]);//先将孩子节点入p
Dfs(v[index][i],p);//深搜孩子节点
p.pop_back(); //记得回溯!!
}
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
int k;
cin>>k;
while(k--){//当k等于0 时,不执行循环,所以不需要特殊考虑
int x;
cin>>x;
v[i].push_back(x);
t[x]=1;//将所有的孩子节点的t都设为1,这样只需要找到不是1的节点,那个节点就是根节点!
}
if(v[i].size()){
sort(v[i].begin(),v[i].end());//每次放完孩子都需要排序,以求得最小的编号
}
}
for(int i=0;i<n;i++){
if(!t[i]){//这一步的目的是找到根节点进行深搜,
vector<int>p;
p.push_back(i);
Dfs(i,p);//从根节点开始搜索,每次更新数组p
break;
}
}
cout<<temp.size()<<endl;
for(int i=0;i<temp.size();i++){
if(!i) cout<<temp[i];
else cout<<" "<<temp[i];
}
}