这类题一般是给出一个图和一组查询,让你判断这组查询(一般是点集)是否是某种图或者某种路径或者某种点集。
首先要考虑针对顶点判别还是针对边判别。
其次是选择邻接向量还是布尔矩阵或者是带权矩阵,我们要根据题目给的点集范围以及判别过程中需要获取点的相关边还是直接获取点和点之间的连通性信息,来选择数据结构。如果需要获取点的相关边,那么使用邻接向量比较方便,如果需要点和点之间的连通信息,使用布尔向量比较方便。如果既需要判断连通性,又需要快速(给出两个点)查询边权,应该使用带权矩阵。如果不需要直接查询两个点之间的边,而图又比较稀疏,那么应该使用邻接向量。
对于判别,可以正向考虑也可以逆向考虑。正向考虑就是满足什么条件的情况下,它是某种概念(比如哈密顿圈)等等,根据这些条件去判别。逆向考虑就是它不满足什么条件时,会降低成什么概念(比如不是圈,那一定是非TS圈,不是简单圈,一定是TS圈,否则是TS简单圈)。从这两个角度去考虑。逆向考虑的时候,如果逻辑有疏漏,可能会导致错误。如果找不出bug,可以考虑换个考虑(正向换逆向,逆向换正向)。
常常要考虑的问题:
对于圈(当然,首先要判断这条路径首位顶点相同,是一个圈),我们常常排除路径的第一个点或者最后一个点然后进行计数,这样计数的时候,所有的顶点都只出现一次,也就意味着,首尾顶点出现了两次。
二重遍历的时候,要考虑是有向图还是无向图,如果是无向图的话是一个组合问题,对于u->v和v->u是等价的,所以我们可以总假设v>u来判别u->v即可。如果是有向图,那么是一个排列问题,显然u->v和v->u不等价。
下面我写的代码有一点点小瑕疵,如果只是需要判断是否经过一个点,使用布尔数组即可,不需要使用整型数组计数。
判断连通性可以使用DFS也可以使用并查集。DFS:从任意一个顶点开始执行一轮DFS,如果还有未被访问的顶点说明不连通。并查集:遍历边将顶点合并,最终连通分量为1说明连通,否则不连通。
如果一个无向图是连通的,且最多只有两个奇点(或者0个),则一定存在欧拉道路。如果有两个奇点,则必须从其中一个奇点出发,另一个奇点终止;如果奇点不存在,则可以从任意点出发,最终一定会回到该点(称为欧拉回路,欧拉回路是特殊的欧拉道路)。
A1122 Hamiltonian Cycle
哈密顿圈的判别
A1126 Eulerian Path
欧拉图的判别,记得判断连通性
A1142 Maximal Clique
A1150 Travelling Salesman Problem
中国邮递员问题
A1146 Topological Order
拓扑序列的判别
A1134 Vertex Cover
这道题要求的是一条边至少有一个顶点被覆盖。因为一个顶点可能有多条边与之相连,从顶点的角度来考虑比较复杂,那么显然应该对边考虑。
这道题应该最好使用BFS寻最短路,群里有人说什么Dijkstra,其实是Dijkstra写多了盲目使用,首先得明白这是一个无向无权图,所以使用BFS就可以了,当然写出来和Dijkstra确实很像。无奈有一个点没有过,所以就不贴代码和思路了。柳婼使用了DFS,在这道题里没问题,但是数据刁钻一点可能就爆了。传送门
基本上就是使用Dijkstra算法,这类题都快写吐了,上手Dijkstra。下面给出常用的几个问题的规律:
// 初始化
fill(distTo, distTo + N, INT_MAX);
fill(timeTo, timeTo + N, INT_MAX / 2);
fill(edgeTo, edgeTo + N, -1);
fill(marked, marked + N, false);
priority_queue<Node> pq;
// 不要处理起点
distTo[source] = 0;
timeTo[source] = 0;
pq.push(Node(source, 0));
int u, v;
while (!pq.empty()) {
u = pq.top().to;
pq.pop();
if(marked[u]) continue;
if(u == destination) break;// 找到终点,退出
marked[u] = true;// 记得标记为true,否则会无限循环
for(Edge& e : G[u]){
v = e.to;
if(marked[v]) continue;// 过滤,否则会无限循环
if(distTo[u] + e.length < distTo[v]){
distTo[v] = distTo[u] + e.length;
edgeTo[v] = u;
timeTo[v] = timeTo[u] + e.time;
pq.push(Node(v, distTo[v]));
}else if(distTo[u] + e.length == distTo[v] && timeTo[u] + e.time < timeTo[v]){
timeTo[v] = timeTo[u] + e.time;
edgeTo[v] = u;
}
}
}
// 获取路径(包括起点和终点)
for(int x = destination; x != -1; x = edgeTo[x]){
path.push_back(x);
}
struct Node{
int to;
int cost;
Node(int to, int cost) : to(to), cost(cost){};
};
bool operator < (Node a, Node b){// 权大的优先级小,在堆的下层
return a.cost > b.cost;
}
一般有两种做法,一种是类似edgeTo这种,在dfs的时候记录每个顶点的上一个顶点。但是参数需要传两个顶点不是很方便,在最外层可以调用dfs(source, -1),用temp[source] = -1代表source是路径的起点。
void dfs(int u, int v){// u为当前要遍历的顶点,v为上一个顶点
temp[u] = v;
//...
for(int w : adj(u){
dfs(w, u);
}
//...
}
还有一种是
void dfs(int u){
if(出口条件){
path.pop_back(u);// 这句一定不能少,在出口处要回溯。
return;
}
path.push_back(u);// 加入顶点
// 逻辑和dfs部分
path.pop_back(u);// 回溯
}
这种其实就是一种回溯写法。
这类题还没有考过,但是甲级的考纲里写的是“最短路径”而不是“单源最短路径”。
做了一个水题,不过感觉如果考不会考太难。
【关键路径】HDU4109 Instrction Arrangement
// 初始化
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
d[i][j] = d[j][i] = oo;
}
d[i][i] = 0;
}
// floyd,只需要更新到n即可
for(int k = 0; k < n; k++){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
【floyd算法】UVA821 Page Hopping
A1003 Emergency 救援最短路径和最大救援部队
A1072 Gas Station
A1087 All Roads Lead to Rome
A1030 Travel Plan
A1018 Public Bike Management
A1111 Online Map
由于PAT甲级已经明确不考动态规划,所以这一类题暂时放一放。而且PAT考察的动态规划类型也不全面。
A1057 Stack|树状数组
1007 Maximum Subsequence Sum| 动态规划
A1045 Favorite Color Stripe|最长不下降子序列
A1040 Longest Symmetric String|最长回文子串
A1068 Find More Coins|0-1背包