Graph theory
一、无向图遍历
e.g. 有向无环图中一个节点的所有祖先
- bitset+adjacent matrix巧解
- 遍历每个节点的所有父节点,找到后让父节点继承该节点的所有子节点
class Solution {
public:
vector<vector<int>> getAncestors(int n, vector<vector<int>>& edges) {
bitset<1000> a[1000];
int len=edges.size();
for(int i=0;i<len;i++)a[edges[i][0]][edges[i][1]]=1;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(a[j][i]){
a[j]|=a[i];
}
}
}
vector<vector<int>> ans;
for(int i=0;i<n;i++){
vector<int> tmp;
for(int j=0;j<n;j++){
if(a[j][i])tmp.push_back(j);
}
ans.push_back(tmp);
}
return ans;
}
};
- DFS遍历
二、拓扑排序
1. 定义
- 拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。
2. 处理方法
Kahn 算法
初始状态下,集合\(S\)中装着所有入度为\(0\)的点,\(L\)是一个空列表。
每次从\(L\)中取出一个点\(u\)放入\(L\), 然后将\(u\)的所有边\(e_{1},e_{2},e_{3}\dots\)删除。对于边\((u,v)\),若将该边删除后点\(v\)的入度变为\(0\),则将\(v\)放入\(L\)中。
重复上述过程,直至集合为空。
例题
e.g.1 杂务
-
John
的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房,还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务\(1\)。John
有需要完成的\(n\)个杂务的清单,并且这份清单是有一定顺序的,杂务 \(k(k>1)\) 的准备工作只可能在杂务 \(1\) 至 \(k-1\) 中。 -
写一个程序从\(1\)到\(n\)读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。当然互相没有关系的杂务可以同时工作,并且,你可以假定
John
的农场有足够多的工人来同时完成任意多项任务。
思路分析
- 典型的拓扑排序,代码实现如下:
int main() {
int n;
scanf("%d", &n);
vector<vector<int>> edges(n + 1);
vector<int> tim(n + 1, 0), f(n + 1, 0);
vector<int> in_degree(n + 1, 0);
queue<int> q;
for (int i = 0; i < n; i++) {
int x = read();
tim[x] = read();
while (1) {
int y = read();
if (!y)break;
edges[y].emplace_back(x);
in_degree[x]++;
}
}
for (int i = 1; i <= n; i++) {
if (in_degree[i] == 0) {
q.emplace(i);
f[i] = tim[i];
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto edge : edges[u]) {
in_degree[edge]--;
if (in_degree[edge] == 0)q.emplace(edge);
f[edge] = max(f[edge], f[u] + tim[edge]);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, f[i]);
}
printf("%d", ans);
return 0;
}
e.g.2 最大食物链计数
思路分析
- 因为食物链是没有环的,所以可以用拓扑排序。最后遍历找出\(0\)出度的结点,累加所能到达该节点的所有最大通路即可。
int n,m;
const int mod=80112002;
int main(){
n=read(),m=read();
vector<vector<int>> edges(n+1);
vector<int> in_degree(n+1,0);
vector<int> out_degree(n+1,0);
for(int i=0;i<m;++i){
int a,b;
a=read(),b=read();
edges[b].emplace_back(a);
in_degree[a]++;
out_degree[b]++;
}
vector<int> dp(n+1,0);
queue<int> q;
for(int i=1;i<=n;++i){
if(!in_degree[i])dp[i]=1,q.emplace(i);
}
while(!q.empty()){
int curr=q.front();
q.pop();
for(auto node:edges[curr]){
in_degree[node]--;
if(!in_degree[node])q.emplace(node);
dp[node]=(dp[node]+dp[curr])%mod;
}
}
int ans=0;
for(int i=1;i<=n;i++){
if(!out_degree[i]){
ans=(ans+dp[i])%mod;
}
}
printf("%d",ans);
return 0;
}
三、存图之链式向前星
class edge{
public: int nxt,to,w; //edge[i].to表示第i条边的终点,edge[i].nxt表示与第i条边同起点的下一条边的存储位置
};
int cnt;
inline void add(int a,int b,int c,vector<edge>& edges,vector<int>& head){
edges[cnt].to=b;
edges[cnt].w=c;
edges[cnt].nxt=head[a]; //指向结点a上一次存的边的位置
head[a]=cnt++; //更新结点a最新边的存放位置
}
int main(){
int m,n;
n=read(),m=read();
vector<edge> edges(m+1);
vector<int> head(n+1); //数组head[]是用来表示以i为起点的第一条边存储的位置
for(int i=1;i<=n;i++){
head[i]=-1;
edges[i].nxt=-1;
}
while(m--){
int x,y,w;
x=read(),y=read(),w=read();
add(x,y,w,edges,head);
}
return 0;
}
四、最短路算法
1. Dijkstra算法
- 模板
class edge{
public: int to,nxt,w;
};
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q;
int cnt;
int n,m,s;
inline void add(int a,int b,int c,vector<edge>& edges,vector<int>& head){
edges[cnt].to=b;
edges[cnt].w=c;
edges[cnt].nxt=head[a];
head[a]=cnt++;
}
void dijkstra(vector<bool> &visit,vector<int> &dis,vector<edge> edges,vector<int> head){
dis[s]=0;
q.emplace(make_pair(0,s));
while(!q.empty()){
pair<int,int> curr=q.top();
q.pop();
int curr_node=curr.second;
if(visit[curr_node])continue;
visit[curr_node]=1;
for(int i=head[curr_node];~i;i=edges[i].nxt){ //遍历头结点所联结的边
int vertex=edges[i].to;
if(dis[vertex]>dis[curr_node]+edges[i].w){ //更新最短距离
dis[vertex]=dis[curr_node]+edges[i].w;
q.emplace(make_pair(dis[vertex],vertex));
}
}
}
}
int main(){
n=read(),m=read(),s=read();
vector<edge> edges(m+1);
vector<int> head(n+1),dis(n+1);
vector<bool> visit(n+1,0);
for(int i=1;i<=n;i++)head[i]=-1,dis[i]=INT_MAX;
while(m--){
int x,y,z;
x=read(),y=read(),z=read();
add(x,y,z,edges,head);
}
dijkstra(visit,dis,edges,head);
for(int i=1;i<=n;i++){
printf("%d ",dis[i]);
}
return 0;
}
模板题的简化写法 无向图的最短路
#define pii pair<int,int>
class edge {
public:
int to, nxt, w;
};
int cnt_e;
inline void add(int a, int b, int w, vector<edge>& edges, vector<int>& head) {
edges[cnt_e].to = b;
edges[cnt_e].nxt = head[a]; //The traverse process is reversed,
edges[cnt_e].w = w; //so each edge record the index of edge which comes before it
head[a] = cnt_e++; //refresh the index that the head vector record
}
int main() {
int n = read(), m = read(), s = read(), t = read();
vector<int> head(n + 1, -1), dis(n + 1, 0x3f3f3f3f);
vector<edge> edges(2 * m + 2);
vector<bool> vis(n + 1, 0);
for (int i = 0; i < m; ++i) {
int u = read(), v = read(), w = read();
add(u, v, w, edges, head);
add(v, u, w, edges, head);
}
priority_queue<pii, vector<pii>, greater<pii>> q;
q.emplace(make_pair(0, s));
dis[s] = 0;
while (!q.empty()) {
pii tmp = q.top();
q.pop();
int cur_node = tmp.second;
if (vis[cur_node])continue;
vis[cur_node] = 1;
for (int i = head[cur_node]; ~i; i = edges[i].nxt) {
int vertex = edges[i].to; //get the node that this edge links to
if (dis[vertex] > dis[cur_node] + edges[i].w) {
dis[vertex] = dis[cur_node] + edges[i].w;
q.emplace(make_pair(dis[vertex], vertex));
}
}
}
printf("%d", dis[t]);
return 0;
}
e.g.1 得到要求路径的最小带权子图
思路
- 从三个点分别来搜,找到共同经过的结点,答案就是三个源到某共同结点和的最小值。
class edge{
public: int to,nxt,w;
};
class Solution {
public:
long long minimumWeight(int n, vector<vector<int>>& ed, int src1, int src2, int dest) {
int m=ed.size();
int cnt=0;
vector<edge> edges_1(m),edges_2(m);
vector<int> head_1(n,-1),head_2(n,-1);
vector<long long> dis_1(n,binf),dis_2(n,binf),dis_3(n,binf);
auto add=[&](int a,int b,int c,vector<edge>& edges,vector<int>& head){
edges[cnt].nxt=head[a];
edges[cnt].to=b;
edges[cnt].w=c;
head[a]=cnt;
};
auto dijkstra=[&](int start,vector<edge> edges,vector<int> head,vector<long long>& dis){
pq<pll,vector<pll>,greater<pll>> q;
vector<bool> visit(n,0);
dis[start]=0;
q.emplace(mkp(0,start));
while(!q.empty()){
auto curr=q.top();
q.pop();
int curr_node=curr.second;
if(visit[curr_node])continue;
visit[curr_node]=1;
for(int i=head[curr_node];~i;i=edges[i].nxt){
int vertex=edges[i].to;
if(dis[vertex]>dis[curr_node]+edges[i].w){
dis[vertex]=dis[curr_node]+edges[i].w;
q.emplace(mkp(dis[vertex],vertex));
}
}
}
};
for(auto e:ed){
add(e[0],e[1],e[2],edges_1,head_1);
add(e[1],e[0],e[2],edges_2,head_2);
cnt++;
}
dijkstra(src1,edges_1,head_1,dis_1);
dijkstra(src2,edges_1,head_1,dis_2);
dijkstra(dest,edges_2,head_2,dis_3);
long long ans=LLONG_MAX;
for(int i=0;i<n;i++){
if(dis_1[i]!=LLONG_MAX&&dis_2[i]!=LLONG_MAX&&dis_3[i]!=LLONG_MAX){
long long tmp=dis_1[i]+dis_2[i]+dis_3[i];
ans=min(tmp,ans);
}
}
return ans==LLONG_MAX?-1:ans;
}
};
e,g.2 带限制的最短路
- 因为有时间限制,所以在更新节点时要注意只有 \(visit_{to}<curTime\) 。
class Solution:
def minCost(self, maxTime: int, edges: List[List[int]], passingFees: List[int]) -> int:
n=len(passingFees)
adj=defaultdict(list)
q=SortedList()
for u,v,t in edges:
adj[u].append((v,t))
adj[v].append((u,t))
q.add((passingFees[0],0,0)) # t,node,cost
visit=[maxTime+1 for i in range(n)]
while q:
cost,time,u=q[0]
q.pop(0)
if time>maxTime or time>=visit[u]:
continue
if u==n-1:
return cost
visit[u]=time
for v,t in adj[u]:
q.add((cost+passingFees[v],t+time,v))
return -1
二、Floyd算法
-
该算法为全源最短路算法,且可以处理负边权(不能处理负环)。算法主要思想是dp,用$O(n^3)$来更新最短路。
- 第一层循环遍历每一个可用的中间节点,接着枚举起点和终点
- 当
dis[x][y]>dis[x][k]+dis[k][y]
时进行状态转移。
//提前将邻接矩阵存在dis数组里,其他不连通的地方初始化成无穷大
for(int k=1;k<=n;++k)//枚举中间点
for(int i=1;i<=n;++i)//枚举起点
if(i!=k)//节省时间,如果一样就不往下走
for(int j=1;j<=n;++j)//枚举终点
if(i!=j&&j!=k)//继续判断,如果有一样的就不往下走
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);//状态转移方程,也就是所谓的松弛操作
e,g.1 模板+加深理解题
int main() {
int n = read(), m = read();
vector<int> time(n + 1);
for (int i = 1; i <= n; ++i)time[i] = read();
vector<vector<int>> grid(n + 1, vector<int>(n + 1, 0x3f3f3f3f));
for (int i = 0; i < m; ++i) {
int u = read(), v = read(), w = read();
grid[u + 1][v + 1] = w;
grid[v + 1][u + 1] = w;
}
int q = read();
int k = 1; //time is monotone increasing
while (q--) {
int u = read(), v = read(), t = read();
for (; k <= n and time[k] <= t; k++) { //if the village has been rebuilt, since t is monotone increasing,
for (int i = 1; i <= n; ++i) { //we only need to update the village that has been rebuilt.
if (k != i) {
for (int j = 1; j <= n; ++j) {
if (i != j and k != j) {
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
}
}
}
}
}
if (time[u + 1] > t or time[v + 1] > t) {
cout << -1 << endl;
}else {
if (grid[u + 1][v + 1] == 0x3f3f3f3f)cout << -1 << endl;
else printf("%d\n", grid[u + 1][v + 1]);
}
}
return 0;
}
三、SPFA
- 这个奇妙的算法可以判断负环。底层原理类似BFS,用BFS进行松弛操作。
- 关于SPFA判断负环:一个有\(n\)个点的图,对于每个点而言,其进行松弛操作,即由该点往外得到的最短路径最多只有\(n-1\)条,所以如该点的松弛操作大于\(n-1\),则有负环。
function<bool(int)>spfa = [&](int start)->bool {
vector<bool> vis(n + 1, 0);
vector<int> cnt(n + 1, 0);
vis[start] = 1;
height[start] = 0;
queue<int> q;
q.emplace(start);
while (!q.empty()) {
int cur = q.front();
q.pop();
vis[cur] = 0;
if (++cnt[cur] == n) {
return 0;
}
for (int i = head[cur]; ~i; i = edges[i].nxt) {
if (edges[i].w + height[cur] < height[edges[i].to]) {
height[edges[i].to] = height[cur] + edges[i].w;
if (!vis[edges[i].to]) {
q.emplace(edges[i].to);
vis[edges[i].to] = 1;
}
}
}
}
return 1;
};
四、Johnson algorithm
- 奇妙的全源最短路算法,可以判断负环。
- 算法实现是先添加一个上帝结点跑一遍SPFA,再用该结果更新其他边的权重:如果有一条从\(u\)到\(v\)的权重为\(w\)的边,我们将其更新为\(w+h_{u}-h_{v}\)
- 接下来跑\(n\)轮Dijkstra就行了
int n, m;
#define pii pair<int,int>
#define inf 1e9
class edge {
public:
int to, nxt, w;
};
int cnt_e;
inline void add(int a, int b, int w, vector<edge>& edges, vector<long long>& head) {
edges[cnt_e].to = b;
edges[cnt_e].nxt = head[a]; //The traverse process is reversed,
edges[cnt_e].w = w; //so each edge record the index of edge which comes before it
head[a] = cnt_e++; //refresh the index that the head vector record
}
signed main() {
n = read(), m = read();
vector<long long> head(n + 1, -1);
vector<edge> edges(m + n + 5);
vector<long long> height(n + 1, inf);
function<bool(int)>spfa = [&](int start)->bool {
vector<bool> vis(n + 1, 0);
vector<int> cnt(n + 1, 0);
vis[start] = 1;
height[start] = 0;
queue<int> q;
q.emplace(start);
while (!q.empty()) {
int cur = q.front();
q.pop();
vis[cur] = 0;
if (++cnt[cur] == n + 1) {
return 0;
}
for (int i = head[cur]; ~i; i = edges[i].nxt) {
if (edges[i].w + height[cur] < height[edges[i].to]) {
height[edges[i].to] = height[cur] + edges[i].w;
if (!vis[edges[i].to]) {
q.emplace(edges[i].to);
vis[edges[i].to] = 1;
}
}
}
}
return 1;
};
for (int i = 0; i < m; ++i) {
int u = read(), v = read(), w = read();
add(u, v, w, edges, head);
}
for (int i = 1; i <= n; ++i) {
add(0, i, 0, edges, head);
}
if (!spfa(0)) {
puts("-1");
return 0;
}
for (int u = 1; u <= n; ++u) {
for (int i = head[u]; ~i; i = edges[i].nxt) {
edges[i].w += height[u] - height[edges[i].to];
}
}
for (int x = 1; x <= n; ++x) {
long long ans = 0;
priority_queue<pii, vector<pii>, greater<pii>> q;
vector<long long> dis(n + 1, inf);
vector<bool> vis(n + 1, 0);
q.emplace(make_pair(0, x));
dis[x] = 0;
while (!q.empty()) {
pii tmp = q.top();
q.pop();
int cur_node = tmp.second;
if (vis[cur_node])continue;
vis[cur_node] = 1;
for (int i = head[cur_node]; ~i; i = edges[i].nxt) {
int vertex = edges[i].to; //get the node that this edge links to
if (dis[vertex] > dis[cur_node] + edges[i].w) {
dis[vertex] = dis[cur_node] + edges[i].w;
q.emplace(make_pair(dis[vertex], vertex));
}
}
}
for (int j = 1; j <= n; ++j) {
if (dis[j] == inf) {
ans += j * 1e9;
}
else {
ans += j * (dis[j] + height[j] - height[x]);
}
}
printf("%lld\n", ans);
}
return 0;
}
五、欧拉路
1. 欧拉路径
e.g.1 求有向图最小字典序的路径
- 主要算法思想蛮简单,根据有向图欧拉路的充要条件找到起点和终点。由于本题求的是字典序最小的路径,那么我们只需要用一个小根堆存边即可,最后DFS解决。
int m, n;
vector<priority_queue<int,vector<int>,greater<int>>> g;
vector<int> in_degree, out_degree;
stack<int> st;
void dfs(int start) {
while (!g[start].empty()) {
int curr = g[start].top();
g[start].pop();
dfs(curr);
}
st.emplace(start);
}
int main() {
n = read(), m = read();
g.resize(n + 1);
in_degree.resize(n + 1), out_degree.resize(n + 1);
while (m--) {
int u = read(), v = read();
g[u].emplace(v);
in_degree[v]++, out_degree[u]++;
}
bool flag = 1;
int start = 1;
vector<int> cnt(2, 0);
for (int i = 1; i <= n; i++) {
if (in_degree[i] != out_degree[i])flag = 0;
if (in_degree[i] - out_degree[i] == 1) {
cnt[0]++;
}
if (out_degree[i] - in_degree[i] == 1) {
start = i;
cnt[1]++;
}
}
if (!flag && !(cnt[0] == cnt[1] && cnt[0] == 1))return !printf("No");
dfs(start);
while (!st.empty()) {
printf("%d ", st.top());
st.pop();
}
return 0;
}
六、最小生成树
1. Kruskal算法
该算法主要是运用了贪心的思想。先把边按照权值进行排序,用贪心的思想优先选取权值较小的边,并依次连接,若出现环则跳过此边(用并查集来判断是否存在环)继续搜,直到已经使用的边的数量比总点数少一即可。
class edge{
public: int u,v,w;
};
vector<int> father;
vector<edge> edges;
int n,m,ans,cnt;
inline int find(int x){
while(x!=father[x])x=father[x]=father[father[x]]; //节省栈空间
return x;
}
void kruskal(){
sort(edges.begin(),edges.end(),[&](edge a,edge b){return a.w<b.w;});
for(int i=0;i<m;i++){
int u_fa=find(edges[i].u),v_fa=find(edges[i].v);
if(u_fa==v_fa)continue; //两人祖先相同,说明两节点联通,则两节点不能相连(恋)
ans+=edges[i].w;
father[v_fa]=u_fa;
if(++cnt==n-1)break;
}
}
int main(){
vector<int> de(n+1,0);
n=read(),m=read();
father.emplace_back(0);
for(int i=1;i<=n;i++){
father.emplace_back(i);
}
for(int i=0;i<m;i++){
int x=read(),y=read(),z=read();
edge tmp;
tmp.u=x,tmp.v=y,tmp.w=z;
edges.emplace_back(tmp);
}
kruskal();
if(cnt==n-1)printf("%d",ans);
else printf("orz");
return 0;
}
2. Prim算法
Prim的算法思想其实蛮简单的,与Dijkstra很像。有些细节还是要想一想的,所以还是用Kruskal。
class edge {
public: int to, w, next;
};
int n, m, cnt, ans, k;
vector<int> dis, head;
vector<bool> vis;
vector<edge> edges;
inline void add(int u, int v, int w) {
edges[k].to = v;
edges[k].w = w;
edges[k].next = head[u]; //以该边起点为起点的下一条边
head[u] = k++;
}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
void prim() {
dis[1] = 0;
q.emplace(make_pair(0, 1)); //first->dis,second->node
while (!q.empty() and cnt < n) {
int cur_dis = q.top().first, cur_node = q.top().second;
q.pop();
if (vis[cur_node])continue;
cnt++;
ans += cur_dis;
vis[cur_node] = 1;
for (int i = head[cur_node]; i != -1; i = edges[i].next) {
if (edges[i].w < dis[edges[i].to]) {
dis[edges[i].to] = edges[i].w;
q.emplace(make_pair(dis[edges[i].to], edges[i].to));
}
}
}
}
int main() {
n = read(), m = read();
dis.resize(n + 1), head.resize(n + 1), vis.resize(n + 1);
fill(vis.begin(), vis.end(), 0);
fill(dis.begin(), dis.end(), INT_MAX);
fill(head.begin(), head.end(), -1);
edges.resize(2 * m);
for (int i = 0; i < m; i++) {
int u = read(), v = read(), w = read();
add(u, v, w);
add(v, u, w);
}
prim();
if (cnt == n)printf("%d", ans);
else printf("orz");
return 0;
}
讲了这么多,来几道例题吧!
e.g.1 连接所有点的最小费用
- 题目评价:基本上是一道裸题,没啥好讲的。
class Solution {
public:
vector<int> father;
int dist(int x_1,int y_1,int x_2,int y_2){
return abs(x_1-x_2)+abs(y_1-y_2);
}
inline int find(int x){
while(x!=father[x]){
x=father[x]=father[father[x]];
}
return x;
}
class edge{
public:int u,v,dis;
};
int minCostConnectPoints(vector<vector<int>>& points) {
int len=points.size();
father.resize(len);
iota(father.begin(),father.end(),0);
vector<edge> edges;
vector<pair<int,int>> nodes;
for(int i=0;i<len;++i){
nodes.emplace_back(make_pair(points[i][0],points[i][1]));
}
int cnt=0;
for(int i=0;i<len;i++){
for(int j=i+1;j<len;++j){
edge tmp;
tmp.u=i,tmp.v=j;
tmp.dis=dist(nodes[i].first,nodes[i].second,nodes[j].first,nodes[j].second);
edges.emplace_back(tmp);
cnt++;
}
}
sort(edges.begin(),edges.end(),[&](edge& a,edge& b){return a.dis<b.dis;});
int ans=0;
int lim=0;
for(int i=0;i<cnt;i++){
int u_fa=find(edges[i].u),v_fa=find(edges[i].v);
if(u_fa==v_fa)continue;
father[u_fa]=v_fa;
ans+=edges[i].dis;
if(++lim==len-1)break;
}
return ans;
}
};
e.g.2 (难题) 无线通讯网
国防部计划用无线网络连接若干个边防哨所。2种不同的通讯技术用来搭建无线网络;
每个边防哨所都要配备无线电收发器;有一些哨所还可以增配卫星电话。
任意两个配备了一条卫星电话线路的哨所(两边都有卫星电话)均可以通话,无论他们相距多远。而只通过无线电收发器通话的哨所之间的距离不能超过\(D\),这是受收发器的功率限制。收发器的功率越高,通话距离\(D\)会更远,但同时价格也会更贵。
收发器需要统一购买和安装,所以全部哨所只能选择安装一种型号的收发器。换句话说,每一对哨所之间的通话距离都是同一个\(D\)。你的任务是确定收发器必须的最小通话距离\(D\),使得每一对哨所之间至少有一条通话路径(直接的或者间接的)。
- 这道题很难是因为思路很难想,我们要让所有哨所可以通话且花费最小,很容易想到是用最小生成树。我们理解一下题意,题目是让我们构建一个生成树,其中有\(S\)条边是不用连上的,而剩下的边的权重是相等的,让我们求出剩下边的最小权重是多少。
- 经过一通分析(
看题解),我们想到了,如果我们将最小生成树的最大的\(S\)条边剪去,剩下的最大边权即为所求(其实还需要证明,\(n\)阶最小生成树剪去大于\(d\)的\(k\)条边后,剩下的正好是\(n-k\)个连通分支)。 - 代码最好用Kruskal,因为好写。
int s, p;
vector<int> father;
class edge {
public:int u, v;
double dist;
};
vector<edge> edges;
vector<pair<int, int>> nodes;
inline int find(int x) {
while (x != father[x]) x = father[x] = father[father[x]];
return x;
}
inline double dist(int x_1, int y_1, int x_2, int y_2) {
return sqrt((x_1 - x_2) * (x_1 - x_2) + (y_1 - y_2) * (y_1 - y_2));
}
int main() {
s = read(), p = read();
father.resize(p + 1);
iota(father.begin(), father.end(), 0);
for (int i = 0; i < p; ++i) {
int x = read(), y = read();
nodes.emplace_back(make_pair(x, y));
}
int cnt = 0;
for (int i = 0; i < p; ++i) {
for (int j = i + 1; j < p; ++j) {
edge tmp;
tmp.u = i;
tmp.v = j;
tmp.dist = dist(nodes[i].first, nodes[i].second, nodes[j].first, nodes[j].second);
edges.emplace_back(tmp);
cnt++;
}
}
vector<double> ans;
int lim = 0;
sort(edges.begin(), edges.end(), [&](edge& a, edge& b) {return a.dist < b.dist; });
for (int i = 0; i < cnt; ++i) {
int u_fa = find(edges[i].u), v_fa = find(edges[i].v);
if (u_fa == v_fa)continue;
ans.emplace_back(edges[i].dist);
father[v_fa] = u_fa;
if (++lim == p - 1)break;
}
printf("%.2lf", ans[lim - s]);
return 0;
}
e,g.3 有一定思维难度的题
- 由于本身有几条边是连上的,所以我们在加边的时候可以给这几条边的权重设为0,具体做法其实只要在所有边加完之后将这些额外的边再加一边就行了,毕竟在Kruskal的时候里外里是要排序的,权重为0的边一定是最先选择的。
vector<int> father;
class edge {
public:
double u, v, w;
};
inline int find(int x) {
while (x != father[x])x = father[x] = father[father[x]];
return x;
}
inline double dis(double x_1, double y_1, double x_2, double y_2) {
return (double)sqrt((double)((x_1 - x_2) * (x_1 - x_2)) + (double)((y_1 - y_2) * (y_1 - y_2)));
}
vector<edge> edges;
int main() {
int n = read(), m = read();
vector<pair<double, double>> nodes(n+1);
father.resize(n + 1);
iota(father.begin(), father.end(), 0);
for (int i = 1; i <= n; i++) {
double x = read(), y = read();
nodes[i]=make_pair(x, y);
}
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
edge tmp;
tmp.u = i, tmp.v = j;
tmp.w = dis(nodes[i].first, nodes[i].second, nodes[j].first, nodes[j].second);
edges.emplace_back(tmp);
}
}
for (int i = 1; i <= m; i++) {
edge tmp;
tmp.u = read(), tmp.v = read();
tmp.w = 0.00;
edges.emplace_back(tmp);
}
sort(edges.begin(), edges.end(), [&](auto& a, auto& b) {return a.w < b.w; });
int len = edges.size();
double ans = 0;
int cnt = 0;
for (int i = 0; i < len; i++) {
int u_fa = find(edges[i].u), v_fa = find(edges[i].v);
if (u_fa == v_fa)continue;
father[u_fa] = v_fa;
ans += edges[i].w;
if (++cnt == n - 1)break;
}
printf("%.2lf", ans);
return 0;
}
七、基环树
- 基环树:由一个树加一条边使之包含一个环的结构称为基环树。
- 内向基环树:一个基环树状的的有向图,特点是每个结点只有一个出度,并且环外结点指向环内。
- 外向基环树:每个点有且只有一个入度。
e,g.1 Directed roads
- 这题要求找到所有能使图中不出现环的方案数(将所有无向边改为有向边)。
- 显而易见 \(ans=2^{N-len}*(2^{len}-2)\) ,但其实这么推有个问题,那就是该图可能是个基环森林而不是一整个强连通块。
- 于是我们对公式进行修正,\(ans=2^{N-\sum_{i=1}^{n}len_{i}}*\prod_{i=1}^{n}(2^{len_{i}}-2)\)。
inline int64 fast_pow(int64 a,int64 b,int m){
int64 ret=1;
while(b){
if(b&1){
ret=((ret%m)*(a%m))%m;
}
a=((a%m)*(a%m))%m;
b>>=1;
}
return ret;
}
void solve() {
vector<int> vis(n+1),dep(n+1);
vector<vector<int>> edges(n+1);
vector<int> ring;
for(int i=1;i<=n;i++){
int tmp;
cin>>tmp;
edges[i].epb(tmp);
}
function<void(int,int)> dfs=[&](int start,int depth){
dep[start]=depth;
vis[start]=1;
for(int& u:edges[start]){
if(!vis[u]){
dfs(u,depth+1);
}
else if(vis[u]==1){
ring.epb(dep[start]-dep[u]+1);
}
}
vis[start]=2;
return;
};
for(int i=1;i<=n;i++){
if(!vis[i])dfs(i,1);
}
int64 len_sum=0;
int64 mul=1;
for(int& c:ring){
len_sum+=c;
mul=mul*(fast_pow(2,c,MOD)-2+MOD)%MOD;
}
ans=mul*(fast_pow(2,(n-len_sum),MOD)%MOD)%MOD;
cout<<ans%MOD;
return;
}
八、最近公共祖先(LCA)
- 该算法不同于朴素记录路径再一级一级跳上去,我们使用了倍增的思想来完成向上跳的过程。
- 首先我们预处理出深度和 \(ances_{i,j}\) 表示节点 \(i\) 向上 \(2^{j}\) 层的节点。
- 这样我们每次处理询问的时候只需要做如下几步即可:
- 将 \(a\text{, }b\) 转换到同层。
- 接下来,我们选中 \(a\) 节点自上往下跳直到跳到不同时是 \(a\text{, }b\) 的祖先节点为止,这时我们将 \(a\text{, }b\) 更新至 \(ances_{a,k}\text{, }ances_{b,k}\) ,继续上述操作。
- 整体操作评价:太妙了!
vector<int> logn;
void GetLog(int n) {
logn.resize(n + 1);
for (int i = 1; i <= n; i++) {
logn[i] = logn[i - 1] + (1 << logn[i-1] == i);
}
return;
}
void dfs(int cur, int ban) {
ances[cur][0] = ban; // 将上一个节点设置为父节点
dep[cur] = dep[ban] + 1;
for (int i = 1; i <= logn[dep[cur]]; i++) {
ances[cur][i] = ances[ances[cur][i - 1]][i - 1]; // 倍增处理所有的父节点
}
for (int to : edges[cur]) {
if (to != ban) {
dfs(to, cur);
}
}
return;
}
int64 query(int a, int b) {
if (dep[a] < dep[b])swap(a, b); //假设 a 深度大于 b
while (dep[a] > dep[b]) {
a = ances[a][logn[dep[a] - dep[b]] - 1];
}
if (a == b)return a;
for (int k = logn[dep[a]] - 1; k >= 0; k--) { //自上而下跳
if (ances[a][k] != ances[b][k]) { //如果不一样说明最近层在这上面
a = ances[a][k];
b = ances[b][k];
}
}
return ances[a][0];
}
void solve() {
int64 s;
cin >> n >> m >> s;
edges.resize(n + 1);
GetLog(n + 1);
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
edges[x].epb(y);
edges[y].epb(x);
}
dfs(s, 0);
while (m--) {
int a, b;
cin >> a >> b;
cout << (int64)query(a, b) << "\n";
}
return;
}