存图
(邻接矩阵只适用于没有重边(或重边可以忽略)的情况,一般情况下不用考虑)
- 邻接矩阵
空间复杂度高:O(n^2)
查询一条边的存在情况快: O(1)
遍历一个点的所有出边 :O(n)
遍历整个图 :O(n^2)
- 邻接表
空间复杂度低 :为O(m) m为边数
查询某条边 : O(d+(u))即这个点的边数 ,如果边有排序,则通过二分就是O(log d+(u)) log边数的事件。
遍历一个点的所有出边 :O(d+(u))即该点的边数
遍历整个图 :O(n+m) 事件为点的个数加边数。
- 链式前向星
与邻接表基本一致。
优点是边有编号(在更厉害点的算法中有用)
遍历图
dfs
dfs需要解决的最关键问题:
当无法继续往下遍历时,如何回退到上一步
方法:看到关键字回退,应该想到递归,递归就是一步步往下处理,处理到末尾再一步步回退,与dfs很契合
对于dfs过程中的结点,有先入后出的特点,FILO,即栈。
(栈这里不懂看这篇https://blog.csdn.net/saltriver/article/details/54429068)
下面演示用dfs数出图中与源点距离大于d的点的个数
void dfs(ll u,ll step){
if(step>d) ans++;
for(ll i=head[u];i!=-1;i=edge[i].next){//链式前向星存储
dfs(edge[i].to,step+1);
}
return ;
}
灵活运用(dfs思维好题)
题目链接 https://www.luogu.com.cn/problem/P3916
题意 给出N个点,M条边的有向图,对于每个点v,求A(v)表示从点v出发,能到达的编号最大的点。
思路 编号最大的点,我们不妨从最大的点开始遍历,反向建边,这样子从最大的点遍历到的所有点,就都是以该点作为最大值
灵活运用(较简单dfs)
题目链接 https://www.luogu.com.cn/problem/P1123
题意 一个N×M的由非负整数构成的数字矩阵,你需要在其中取出若干个数字,使得取出的任意两个数字不相邻(若一个数字在另外一个数字相邻8个格子中的一个即认为这两个数字相邻),求取出数字和最大是多少。
对于100%的数据,N, M≤6,T≤20,T是样例数
思路 简单dfs
灵活运用(dfs思维好题)
题目链接 https://ac.nowcoder.com/acm/contest/188/C?&headNav=www&headNav=acm&headNav=acm&headNav=acm
题意 小w有一张n个点n-1条边的无向联通图,每个点编号为1~n,每条边都有一个长度,小w现在在点x上,她想知道从点x出发经过每个点至少一次,最少需要走多少路
思路 只有从起点到各个结点中,最长的那个路径需要走一次,其他路径都需要走两次
void dfs(ll u,ll uu,ll length){
ans=max(ans,length);
for(ll i=head[u];~i;i=edge[i].next){
if(edge[i].to==uu) continue;
dfs(edge[i].to,u,length+edge[i].w);
}
}
灵活运用(细节dfs)
题目链接 https://vjudge.net/problem/HDU-2821
题意 有一个R*C的方格,‘.’代表空地,‘az’分别代表该处有126个箱子,某人可以从距离箱子至少一个空格处推箱子,推一次此处少一个箱子,如果这个格还有其他箱子,则和它下一个格的箱子合并或到下一个格,朝着某个方向一直推到边界或者遇到箱子不能推为止(即箱子数多于1的堆在边界)才可以换方向,任意输出一种可以把箱子推完的方案,输出推箱子时起点的位置以及推箱子时的方向。
注意:①起点不能有箱子。②必须要隔一个位置才能碰。③碰的箱子在边上时,剩下的箱子不能移出矩形范围。④格子在边上时,碰之后如果还有格子剩余,pusher的位置就不在边上,否则就在边上。
思路 细节dfs,先预处理下,将’.'换成数字0,abcd换成对应数字即可
踩坑 别用getchar()一个个读入处理,容易出错!!!
小技巧 双重循环还可以这样退出!!!
ll dfs(ll x,ll y,ll cnt,string s,ll d,ll b){//d 1 2 3 4,上右下左,b为间隙数。dfs找到返回1,否则返回0
if(x<1 || x>r || y<1 || y>c) return 0;//当前越界
if(cnt==0){//箱子全部清除
outs=s;
return 1;
}
if(d==0){//d为0,选择方向
for(ll i=1;i<=4;i++){
if(dfs(x+dir[i][0],y+dir[i][1],cnt,s+ar[i],i,b+1)) return 1;
}
}
else{//d不为0
if(gra[x][y]==0){//当前位置是道路
if(dfs(x+dir[d][0],y+dir[d][1],cnt,s,d,b+1)) return 1;
}
else{//当前位置是与木块重合,即撞到木块,
if(b<2) return 0;//如果没有间隙,即必须要隔一个位置才能碰
if(x+dir[d][0]<1 || x+dir[d][0]>r || y+dir[d][1]<1 || y+dir[d][1]>c){//如果后面是边界
ll t=gra[x][y];
if(t>1) return 0;//如果箱子数比1多
gra[x][y]=0;
if(dfs(x,y,cnt-1,s,0,0)) return 1;
gra[x][y]=1;
}
else{//如果后面不是边界
ll t=gra[x][y];
gra[x][y]=0;
gra[x+dir[d][0]][y+dir[d][1]]+=t-1;
if(dfs(x,y,cnt-1,s,0,0)) return 1;
gra[x][y]=t;//回溯
gra[x+dir[d][0]][y+dir[d][1]]+=1-t;
}
}
}
return 0;
}
bfs
结点性质符合FIFO,即队列
放入0点到队列里。
while(队列不为空){
取出队列里的一个点为A。
弹出A
for(遍历A点所有相连的点){
if(该点没有被访问过) {
记录该点访问过
该点入队列
}
}
}
最短路
- 最短路需要考虑的几种情况
Dijkstra
- 适用情况 此算法不能用于求负权图,要求所有边的权重都为非负值。
- 思路 在最短路径的问题中,局部最优解即当前的最短路径或者说是在当前的已知条件下起点到其余各点的最短距离。
- 邻接矩阵暴力模板 时间O(n*n)
//邻接矩阵做法,时间复杂度O(n*n)----------------------------------------------------------
/***
* @Practice Win
* HDU - 2544
* 每次从dis[i]中选择最小的加入集合S
*/
#include<bits/stdc++.h>
#pragma comment(linker, "/STACK:102400000,102400000")//解决递归函数多次调用栈溢出问题
using namespace std;
#define Debug(x) cout<<#x<<':'<<x<<endl
typedef long long ll;
#define INF 0x7fffffff//10^9级别,不到2^32
const ll maxn=1e2+11;//最多顶点数!!!
ll dis[maxn],vis[maxn],gra[maxn][maxn];//自己到自己和不存在都视为INF
ll n,m,start,goal;
ll dijkstra(){//若图连通,返回最短路权值和,否则,返回一个未知值。
for(ll i=1;i<=n;i++){
dis[i]=gra[start][i];
}
vis[start]=1;
ll pos;
for(ll i=2;i<=n;i++){
ll minn=INF;
pos=-1;
for(ll j=1;j<=n;j++) if(!vis[j] && dis[j]<minn) minn=dis[j],pos=j;//顶点编号从1开始!!!
if(pos==-1) break;
vis[pos]=1;
for(ll j=1;j<=n;j++) if(!vis[j] && dis[pos]+gra[pos][j]<dis[j]) dis[j]=dis[pos]+gra[pos][j];//顶点编号从1开始!!!
}
if(pos==-1) cout<<"orz"<<endl;
else cout<<dis[goal]<<endl;
return dis[goal];
}
void init(){
memset(gra,0x3f3f3f3f,sizeof(gra));
memset(vis,0,sizeof(vis));
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
while(cin>>n>>m && (n||m)){
init();
start=1,goal=n;
for(ll i=1;i<=m;i++){//顶点编号从1开始!!!
ll u,v,w;
cin>>u>>v>>w;
if(w<gra[u][v]) gra[u][v]=gra[v][u]=w;//无向图,包含重边情况取最小边考虑!!!
}
dijkstra();
}
return 0;
}
- 邻接矩阵优先队列优化 时间O(n*logn)
//优先队列优化,时间复杂度O(n*logn)-----------------------------------------------------------------
/***
* @Practice Win
* 洛谷 P4779 【模板】单源最短路径(标准版)
* 每次从dis[i]中选择最小的加入集合S
*/
#include<bits/stdc++.h>
#pragma comment(linker, "/STACK:102400000,102400000")//解决递归函数多次调用栈溢出问题
using namespace std;
#define Debug(x) cout<<#x<<':'<<x<<endl
typedef long long ll;
#define INF 0x7fffffff//10^9级别,不到2^32
const ll maxn=1e6+11;//最多边数!!!
struct Edge{
ll fr,to,w,next;
}edge[maxn];
struct Point{
ll w,pos;
friend bool operator <(const Point& a,const Point& b){
return a.w>b.w;//优先队列这里是按值从高到低排序,所以得改为>,才变成从小到大排序
}
};
ll dis[maxn],vis[maxn],head[maxn];//自己到自己和不存在都视为INF
ll n,m,start,goal,tmp;
priority_queue<Point> dq;
ll dijkstra(){//若图连通,返回最短路权值和,否则,返回一个未知值。
for(ll i=head[start];~i;i=edge[i].next){
ll v=edge[i].to;
dis[v]=min(dis[v],edge[i].w);
Point p;
p.pos=v;p.w=dis[v];
dq.push(p);
}
vis[start]=1;
ll pos;
for(ll i=2;i<=n;i++){
while(vis[dq.top().pos]) dq.pop();//考虑队列中有重边情况
pos=dq.top().pos;
dq.pop();
vis[pos]=1;
for(ll j=head[pos];~j;j=edge[j].next){
ll v=edge[j].to;
if(!vis[v] && dis[pos]+edge[j].w<dis[v]){
Point p;
dis[v]=dis[pos]+edge[j].w;//顶点编号从1开始!!!
p.pos=v;p.w=dis[v];
dq.push(p);
}
}
}
return dis[goal];
}
void add(ll u,ll v,ll w){
edge[tmp].fr=u; edge[tmp].to=v; edge[tmp].w=w;
edge[tmp].next=head[u];
head[u]=tmp++;
}
void init(){
memset(head,-1,sizeof(head));
memset(dis,0x3f3f3f3f,sizeof(dis));
memset(vis,0,sizeof(vis));
tmp=1;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m>>start;
init();
for(ll i=1;i<=m;i++){//顶点编号从1开始!!!
ll u,v,w;
cin>>u>>v>>w;
add(u,v,w);//有向图
//add(v,u,w);
}
dijkstra();
for(ll i=1;i<=n;i++){
if(start==i) cout<<0<<" ";
else cout<<dis[i]<<" ";
}
cout<<endl;
return 0;
}
灵活运用(最短路建图+弱思维)
题目链接 https://vjudge.net/problem/POJ-1062
题意
小花想娶部落里的王子 王子的父亲要求一定数量的金币才能娶
困窘的小花还有另外一条出路就是得到另外一件东西与王子的父亲交换 以此来减少金币
同理 可以由别的东西来换取现在需要的东西的优惠
另一个限制条件就是 等级相差k的人不能进行交易 即使是间接交易
思路
关键在于建图,以及对等级限制的处理
物品的交换流程可以构成一个有向图,而且这个图是有边权的。
于是就转化为(求各点到源点最短的距离+各点本身价格)的最小值
枚举每个结点即可。
关于等级限制
在Dijkstra期间将违背等级的点之间的边权设为无限大即可,表示不可访问
ll dijkstra(){//若图连通,返回最短路权值和,否则,返回一个未知值。
for(ll i=1;i<=n;i++){
dis[i]=gra[start][i];
levell[i]=min(l[1],l[i]);
levelr[i]=max(l[1],l[i]);
if(abs(l[i]-l[1])>m) dis[i]=INF;//路径上的等级极值差如果大于m
}
vis[start]=1;
ll pos;
for(ll i=2;i<=n;i++){
ll minn=INF;
pos=-1;
for(ll j=1;j<=n;j++) if(!vis[j] && dis[j]<minn) minn=dis[j],pos=j;//顶点编号从1开始!!!
if(pos==-1) break;
vis[pos]=1;
for(ll j=1;j<=n;j++){
if(!vis[j] && dis[pos]+gra[pos][j]<dis[j]){//顶点编号从1开始!!!
dis[j]=dis[pos]+gra[pos][j];
levell[j]=min(levell[pos],l[j]);
levelr[j]=max(levelr[pos],l[j]);
if(levelr[j]-levell[j]>m) dis[j]=INF;//路径上的等级极值差如果大于m
}
}
}
return 1;
}
Floyd
- 适用情况 适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)
- 思路 这个思想不太好解释,大概思路就是全局的一个维护最短路径,每次增加一个结点,进行一次全局的维护,当所有结点都增加时,得到一个全局的最短路径。因为每次增加一个结点,都是进行一个全局最短路径的维护,所以能够保证最后得到的是全局的最短路径。
灵活运用(弱思维)
题目链接 https://www.luogu.com.cn/problem/P1364
题意 设有一棵二叉树,如图:其中,圈中的数字表示结点中居民的人口。圈边上数字表示结点编号,现在要求在某个结点上建立一个医院,使所有居民所走的路程之和为最小
思路 floyd运用下就行了
/***
* @Practice Win
* 洛谷 P1364 医院设置
* floyd
*/
#include<bits/stdc++.h>
#pragma comment(linker, "/STACK:102400000,102400000")//解决递归函数多次调用栈溢出问题
using namespace std;
#define Debug(x) cout<<#x<<':'<<x<<endl
typedef long long ll;
#define INF 0x7fffffff//10^9级别,不到2^32
const ll maxn=1e2+11;
ll flo[maxn][maxn],cnt[maxn];
ll n;
void floyd(){//flo初始化为一个邻接矩阵之后,即可调用,注意点的编号是否从1开始。
//flo[x][y]表示x到y的最短距离
for (ll k = 1; k <= n; k++) {
for (ll x = 1; x <= n; x++) {
for (ll y = 1; y <= n; y++) {
flo[x][y] = min(flo[x][y], flo[x][k] + flo[k][y]);
}
}
}
}
void solve(){
ll ans=INF;
for(ll i=1;i<=n;i++){
ll minn=0;
for(ll j=1;j<=n;j++){
minn+=cnt[j]*flo[i][j];
}
ans=min(ans,minn);
}
cout<<ans<<endl;
}
void init(){
memset(flo,0x3f3f3f3f,sizeof(flo));
for(ll i=1;i<=n;i++) flo[i][i]=0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
init();
for(ll i=1;i<=n;i++){
cin>>cnt[i];
ll l,r;
cin>>l>>r;
flo[i][l]=flo[l][i]=1;
flo[i][r]=flo[r][i]=1;
}
floyd();
solve();
return 0;
}
最小生成树
kruskal
- 思路 贪心,每次选择权值最小的边,保证每次加入新边不会生成环
- 难点 如何判定生成环,方法:保证新加入的边的两个端点,不都在已加入的边的端点集合中,用并查集实现
- 链式前向星模板 时间O(m*logm)(m为边数)
//链式前向星(kruskal用邻接矩阵不太好写)-----------------------------------
/***
* @Practice Win
* 洛谷 P3366 【模板】最小生成树
* 最小生成树 板子题
*/
#include<bits/stdc++.h>
#pragma comment(linker, "/STACK:102400000,102400000")//解决递归函数多次调用栈溢出问题
using namespace std;
#define Debug(x) cout<<#x<<':'<<x<<endl
typedef long long ll;
#define INF 0x7fffffff//10^9级别,不到2^32
const ll maxn=1e4+11;//最多点数!!!
const ll maxm=1e6+11;//最多边数!!!
struct Edge{
ll fr,to,w,next;
}edge[maxm];//边集,下标从1开始!!!
ll fa[maxn],head[maxn];
ll n,m,tmp;
void add(ll u,ll v,ll w){
edge[tmp].fr=u; edge[tmp].to=v; edge[tmp].w=w;
edge[tmp].next=head[u];
head[u]=tmp++;
}
bool cmp(Edge a,Edge b){
return a.w<b.w;
}
ll found(ll x){
return fa[x]==x?x:fa[x]=found(fa[x]);
}
ll kruskal(){//若存在最小生成树,返回其最小权值和,并输出,若不存在,返回一个错误值,并输出“orz”
sort(edge+1,edge+tmp,cmp);
ll cnt=0,ans=0;
for(ll i=1;i<tmp;i++){//边编号从1开始!!!
ll fu=found(edge[i].fr);//并查集得到fu和fv
ll fv=found(edge[i].to);
if(fu==fv) continue;//若在同一集合,则跳过,说明这两个点都已被使用过
fa[fu]=fv;//合并
cnt++;
ans+=edge[i].w;
if(cnt==n-1) break;
}
if(cnt==n-1) cout<<ans<<endl;
else cout<<"orz"<<endl;
return ans;
}
void makset(){
for(ll i=1;i<=n;i++) fa[i]=i;//点的编号应与题目相符合!!!
}
void init(){
memset(head,-1,sizeof(head));
tmp=1;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
init();
makset();
for(ll i=1;i<=m;i++){
ll u,v,w;
cin>>u>>v>>w;
add(u,v,w);//有向图
//add(v,u,w);//无向图
}
kruskal();
return 0;
}
灵活运用,唯一最小生成树(思维)
题目链接 https://vjudge.net/problem/POJ-1679
题意 判断最小生成树是否唯一
思路枚举最小生成树的n条边,每次减去一条边,看最小生成树是否存在
prim
- 思路 感觉本质上就是kruskal,与kruskal思路没啥区别,要说区别就是kruskal过程上是加边,这个是过程上加点吧,但其实都是每次选择权值最小的边,都是贪心。
- 具体方法 弄两个点集,一个u,一个v,每次选择一个端点在u,一个端点在v产生的边中,权值最小的边,并把在v中的点,加入u中。
- 链式前向星模板 时间O(n*n)
//链式前向星-----------------------------------------------------------------------------
/***
* @Practice Win
* 畅通工程 HDU - 1863
* prim
* 最小生成树 板子题
*/
#include<bits/stdc++.h>
#pragma comment(linker, "/STACK:102400000,102400000")//解决递归函数多次调用栈溢出问题
using namespace std;
#define Debug(x) cout<<#x<<':'<<x<<endl
typedef long long ll;
#define INF 0x7fffffff//10^9级别,不到2^32
const ll maxn=1e2+11;//最大顶点数
struct Edge{
ll fr,to,w,next;
}edge[maxn*maxn];
ll head[maxn],vis[maxn],dis[maxn];
ll n,m,tmp;
ll prim(){//若存在最小生成树,返回其值并输出,否则返回一个未知值,并输出"?"
ll minn=INF,pos,ans=0;
for(ll i=head[1];~i;i=edge[i].next){//从顶点编号1开始初始化
ll v=edge[i].to;
dis[v]=min(dis[v],edge[i].w);//考虑有重边情况!!!
}
vis[1]=1;
for(ll i=2;i<=n;i++){
ll minn=INF;
pos=-1;
for(ll j=1;j<=n;j++) if(!vis[j] && dis[j]<minn) minn=dis[j],pos=j;
if(pos==-1) break;
vis[pos]=1;
ans+=minn;
for(ll i=head[pos];~i;i=edge[i].next){
ll v=edge[i].to;
if(edge[i].w<dis[v]) dis[v]=edge[i].w;
}
}
if(pos==-1) cout<<"?"<<endl;
else cout<<ans<<endl;
return ans;
}
void add(ll u,ll v,ll w){
edge[tmp].fr=u; edge[tmp].to=v; edge[tmp].w=w;
edge[tmp].next=head[u];
head[u]=tmp++;
}
void init(){
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
memset(dis,0x3f3f3f3f,sizeof(dis));
tmp=1;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
while(cin>>m>>n && m){
init();
for(ll i=1;i<=m;i++){
ll u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
prim();
}
return 0;
}
- 邻接矩阵模板 时间O(n*n)
//邻接矩阵-----------------------------------------------------------------------------
/***
* @Practice Win
* 畅通工程 HDU - 1863
* prim
* 最小生成树 板子题
*/
#include<bits/stdc++.h>
#pragma comment(linker, "/STACK:102400000,102400000")//解决递归函数多次调用栈溢出问题
using namespace std;
#define Debug(x) cout<<#x<<':'<<x<<endl
typedef long long ll;
#define INF 0x7fffffff//10^9级别,不到2^32
const ll maxn=1e2+11;//最大顶点数
ll vis[maxn],dis[maxn],gra[maxn][maxn];
ll n,m;
ll prim(){//若存在最小生成树,返回其值并输出,否则返回一个未知值,并输出"?"
ll minn=INF,pos,ans=0;
for(ll i=1;i<=n;i++) dis[i]=gra[1][i];
vis[1]=1;
for(ll i=2;i<=n;i++){
ll minn=INF;
pos=-1;
for(ll j=1;j<=n;j++) if(!vis[j] && dis[j]<minn) minn=dis[j],pos=j;
if(pos==-1) break;
vis[pos]=1;
ans+=minn;
for(ll j=1;j<=n;j++) if(!vis[j] && gra[pos][j]<dis[j]) dis[j]=gra[pos][j];
}
if(pos==-1) cout<<"?"<<endl;
else cout<<ans<<endl;
return ans;
}
void init(){
memset(vis,0,sizeof(vis));
memset(dis,0x3f3f3f3f,sizeof(dis));
memset(gra,0x3f3f3f3f,sizeof(gra));
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
while(cin>>m>>n && m){
init();
for(ll i=1;i<=m;i++){
ll u,v,w;
cin>>u>>v>>w;
if(w<gra[u][v]) gra[u][v]=gra[v][u]=w;
}
prim();
}
return 0;
}
灵活运用(已建立某些边)
继续畅通工程
题意 在有些边已经建立好了的情况下,求最小生成树。
思路 对于已经建立的边,将其权值变为0即可。
灵活运用(已知每个点的代价)
Shichikuji and Power Grid
常见最小生成树是知道每条边的代价,这题增加了一个就是还知道每个点的代价。
题面
有n个城市,坐标为(xi,yi),还有两个系数ci,ki.在每个城市建立发电站需要费用ci.如果不建立发电站,要让城市通电,就需要与有发电站的城市连通。i与j之间连一条无向的边的费用是(ki+kj)*两个城市之间的曼哈顿距离.求让每个城市都通电的最小费用,并输出任意一个方案。
分析
我刚开始想的就是,这不就是prim,然后将dis[i]初始化为每个点的代价嘛,然后发现这是个常见套路,也可以把选每个点的代价转成虚拟原点到这个点的边,不过这样子更好理解。这个套路很常见,但在最小生成树题里还是第一次见到。
城市之间两两连边,边权按题目里提到的计算。然后建立一个虚拟源点,向每个城市i连边,边权为ci.对整个图跑一个最小生成树即可.
-
总结
-
两者区别这个方法的好处就是,因为分成了两个点集合,所以保证了选择的权值最小的边肯定不会产生环,因为这个边的端点分别在两个互斥的集合嘛,不像kruskal选择的边可能都在集合u。
-
稀疏图与稠密图数据结构中对于稀疏图的定义为:有很少条边或弧(边的条数|E|远小于|V|²)的图称为稀疏图(sparse graph),反之边的条数|E|接近|V|²,称为稠密图(dense graph)。此定义来自百度百科,实际上是一种朴素的理解,简单来说边越多,图就越稠密
-
两者复杂度
Kruskal:
Prim: