首先,图论的基础:存储。这里介绍几种存储结构;
邻接矩阵
一种最简单,暴力的存储结构,二维数组存储;
注:这是读入方式的一种,具体看题目。
cpp cin >> n >> m; for (int i=1;i=m;i++) { cin >> i >> j >> x; a[i][j]=a[j][i]=x; }
邻接表(链式前向星)
邻接表,又叫链式前向星,其实就是链表的思路;
先开一个 \(linkk\) 数组,\(linkk[i]\) 表示的是以 \(i\) 为起点第一条边的编号,\(e\) 数组存边,\(e[i].y\) 表示终点,\(e[i].v\) 表示权值,\(e[i].next\) 表示下条边的编号;
邻接表核心就是一个插入函数:
void insert(int x,int y,int v) //x为起点,y为终点,v为权值。
{
e[++t].y=y; e[t].v=v;
e[t].next=linkk[x]; linkk[x]=t;
}
还有一个循环同样重要,类似于查询:
for (int i=linkk[x];i;i=e[i].next)
边表(边集数组)
一种简便的存储结构,思路同样很简单,就是把所有的边存储到 \(e\) 数组中,要存储起点,终点,权值。
struct node
{
int x,y; //起点和终点
int v; //权值
}e[maxm];
dfs遍历
邻接矩阵 \(dfs\) 遍历:
void dfs(int k);
{
printf("%d",k);
f[k]=true;
for (int i=1;i<=n;i++)
if (!f[i] && a[k][i]) dfs(i);
}
邻接表 \(dfs\) 遍历:
void dfs(int k)
{
for (int i=linkk[k];i;i=e[i].next)
if(!vis[e[i].y])
{
vis[e[i].y]=1;
dfs(e[i].y);
}
}
bfs遍历
邻接矩阵 \(bfs\) 遍历:
void bfs(int i);
{
memset(q,0,sizeof(q));
int head=1,tail=1;
q[1]=i; f[i]=true;
while (head<=tail)
{
k=q[head]; cout>>k;
for (int j=1;j<=n,j++)
if (a[k][j] && !f[j])
{
q[++tail]=j;
f[j]=true;
}
head++;
}
}
cpp for (int k=1;k<=n;k++) for (int i=1;i<=n;i++) for (int j=1;j<=n;j++) if (dis[i][k]+dis[k][j]<dis[i][j]) dis[i][j]=dis[i][k]+dis[k][j];
cpp void dijkstra(int st); { memset(f,0,sizeof(f)); memset(dis,0,sizeof(dis)); for (int i=1;i<=n;i++) dis[i]=a[st][i]; f[st]=1; dis[st]=0; for (int i=1;i<n;i++) { int min=0xfffffff, k=0; for (int j=1;j<=n;j++) if (!f[j] && (dis[j]<min)) min=dis[j],k=j; if (k==0) return; //找不到距离最短的点了 f[k]=1; //把k加入集合1; for (int j=1;j<=n;j++) //三角形迭代更新最短距离 if (!f[j] && dis[k]+a[k][j]<dis[j]) dis[j]=dis[k]+a[k][j]; } }
cpp int bellman_ford() { memset(dis,10,sizeof(dis)); //初值 dis[1]=0; //起点到起点的距离为0 for (int i=1;i<=n;i++) //最多迭代n次 { bool p=0; //是否有松弛标记 for (int j=1;j<=tot;j++) //tot条边 { int ax=a[j].x,ay=a[j].y,av=a[j].v; //第j条边起点,终点,长度 if (dis[ax]+av<dis[ay]) //三角形迭代 { dis[ay]=dis[ax]+av; p=1; //松弛标记 } } if (p==0) return 0; //无松弛 } return 1; //有负环 }
cpp void SPFA(int k) //SPFA模板 { memset(dis,10,sizeof(dis)); //初值 memset(vis,0,sizeof(vis)); //清空标记数组,vis[k]表示点k是否在队列中 dis[k]=0; vis[k]=1; int head=1,tail=1; q[head]=k; //点k进队 for (head=1;head<=tail;head++) { int x=q[head]; //取出队头 for (int i=linkk[x];i>0;i=e[i].next) //邻接表查询 { if (dis[x]+e[i].v<dis[e[i].y]) //迭代 { dis[e[i].y]=dis[x]+e[i].v; if (vis[e[i].y]==0) //若此点没有标记,标记,进队 { vis[e[i].y]=1; q[++tail]=e[i].y; } } } vis[x]=0; //出队 } }
cpp void prim(int st) //prim算法 { memset(vis,0,sizeof(vis)); memset(dis,0,sizeof(dis)); for (int i=1;i<=n;i++) dis[i]=a[st][i]; //赋值交叉边 vis[st]=1; int ans=-1e9; for (int i=1;i<=n-1;i++) { int Min=1e9,mini=0; for (int j=1;j<=n;j++) if (!vis[j] && dis[j]<Min) Min=dis[j],mini=j; //找到距离最近的点 vis[mini]=1; //标记 ans+=dis[mini]; //累加边权和 for (int j=1;j<=n;j++) if (!vis[j] && dis[j]>a[mini][j]) { dis[j]=a[mini][j]; //迭代 } } printf("%d",ans); }
cpp void Kruskal(int k) //克鲁斯卡尔 { for (int i=1;i<=n;i++) father[i]=i; //开始每个点的父节点都是它本身 int cnt=0; for (int i=1;i<=m;i++) { int v=getfather(e[i].x); int u=getfather(e[i].y); if (v!=u) //判断e[i].x和e[i].y是否存在于同一集合内 { merge(u,v); //合并 ans+=e[i].v; if (++cnt==n-1) //最小生成树生成结束 { break; } } } }
对有向无环图的点进行先后排序;
这里用队列来维护拓扑序
cpp void Topsort() //拓扑排序 { int head=1,tail=0; for (int i=1;i<=n;i++) if (in[i]==0) q[++tail]=i; //初始化 for (head=1;head<=tail;head++) { int x=q[head]; //取出队头 for (int i=linkk[x];i;i=e[i].next) //邻接表查询 { int y=e[i].y; if (--in[y]==0) q[++tail]=y; //入队 } if (tail==n) return; } }
cpp 注意: 1. 有向图的拓扑序列不唯一; 2. 如果图中存在环,无拓扑序列; 3. 还有一种写法用栈来实现拓扑排序。
背包
具体戳这儿;
递推
这里给一个例子,不再多说
给定一个由 \(n\) 行数字组成的数字三角形,如下图所示:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大(每一步只能从一个数走到下一层上和它最近的左边的数或者右边的数)。
cpp for (int i=1;i<=n;i++) for (int j=1;j<=i;j++) { cin >> a[i][j]; } for (int i=1;i<=n;i++) for (int j=1;j<=i;j++) { f[i][j]=max(f[i-1][j],f[i-1][j-1])+a[i][j]; }
LCS-最长不上升子序列问题、LIS-最长不下降子序列问题
有一道经典的题目囊括了这两个问题:导弹拦截。
为了拦截敌国的袭击,科学家研发出一套导弹系统,导弹系统有个缺陷:第一发炮弹可以到达任意高度,然而之后的每一发炮弹都不能高于前一发的高度。
现给出数个导弹的高度 \(h[i]\) ( \(h[i]\)为\(<=50000\)的正整数 ),计算一套导弹拦截系统最多可以拦截多少导弹,如果需要拦截全部导弹需要多少套导弹拦截系统?
思路:
第一问很显然,就是一个 \(LCS\) 的模板;
第二问是一个 \(LIS\) 的模板,为什么呢?(蒟蒻不会 呜呜呜~)
for (int i=1;i<=n;i++) //LCS
{
dp[i]=1;
for (int j=0;j<=i-1;j++)
if (h[i]<=h[j]) dp[i]=max(dp[i],dp[j]+1);
}
int Max=-0xfffffff;
for (int i=1;i<=n;i++) Max=max(Max,dp[i]);
cout << Max << endl;
for (int i=1;i<=n;i++) //LIS
{
dp[i]=1;
for (int j=0;j<=i-1;j++)
if (h[i]>h[j]) dp[i]=max(dp[i],dp[j]+1);
}
Max=-0xfffffff;
for (int i=1;i<=n;i++) Max=max(Max,dp[i]);
cout << Max << endl;
好像就是一个板子套用一下吧,这里就列一下最经典的石子合并问题。
在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。
for (int i=1;i<=n;i++)
{
cin >> a[i];
sum[i]=sum[i-1]+a[i]; //前缀和,前i个石子堆的总数量
}
memset(f,10,sizeof(f));
for (int i=1;i<=n;i++) f[i][i]=0;
for (int len=2;len<=n;len++) //区间长度
{
for (int i=1;i<=n-len+1;i++) //左端点
{
int j=i+len-1; //右端点
for (int k=i;k<=j;k++)
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]); //i到j的区间最优值通过中间点k来更新
}
}
cout << f[1][n];
二维 \(dp\),最经典的应该还是马拦过河卒了吧,这边拿这道题做例子;
棋盘上 \(A\) 点有一个过河卒,需要走到目标 \(B\) 点。卒行走的规则:可以向下、或者向右。
同时在棋盘上 \(C\) 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。
棋盘用坐标表示,\(A\) 点 \((0, 0)\)、\(B\) 点 \((n, m)(n, m\)为不超过 \(15\) 的整数\()\),同样马的位置坐标是需要给出的。现在要求你计算出卒从 \(A\) 点能够到达 \(B\) 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。
scanf("%d %d %d %d",&n,&m,&x,&y);
n++;m++;x++;y++;
memset(f,0,sizeof(f));
for (int i=0;i<=8;i++)
{
int xx=x+fx[i],yy=y+fy[i];
if (xx>=1 && xx<=n && yy>=1 && yy<=m)
{
f[xx][yy]=1;
}
}
dp[1][1]=1;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
if (f[i][j]==0 && !(i==1 && j==1))
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
printf("%d",dp[n][m]);
while (cin >> ch,ch!='.') a[++n]=ch;
while (cin >> ch,ch!='.') b[++m]=ch;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if (a[i]==b[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
}
cout << dp[n][m];
原文:https://www.cnblogs.com/Michael-zx/p/12357909.html