文章目录
- 写在前面
- [HDU - 4081 Qin Shi Huang's National Road System](https://vjudge.net/problem/HDU-4081)
- [HDU - 2874 Connections between cities](https://vjudge.net/problem/HDU-2874)
- [HDU - 1217 Arbitrage](https://vjudge.net/problem/HDU-1217)
- [HDU - 2224 The shortest path](https://vjudge.net/problem/HDU-2224)
- [HDU - 1142 A Walk Through the Forest](https://vjudge.net/problem/HDU-1142)
- [HDU - 3639 Hawk-and-Chicken](https://vjudge.net/problem/HDU-3639)
写在前面
前段时间做多校,感觉自己的基础不太好,之前学过的一些东西也都忘记了,想趁这个机会回来补一补基础,尤其是图论这一块。因此我打算回来做一些经典的图论算法题,“不积跬步,无以至千里”,还是从基础的开始,一步一步去挑战更难的题目。
HDU - 4081 Qin Shi Huang’s National Road System
题目大意
给你一张 n n n个点的二维平面图,每个点都有其坐标和点权(第 i i i个点的点权为 a i a_i ai),要求一棵连接这 n n n个点的生成树,满足其中一条边连接的两个点的点权之和与除去这条边以外 n − 2 n-2 n−2条边的边权和的比值最大。在本题中边权即为两点之间的欧几里得距离。
数据范围: 3 ≤ n ≤ 1 0 3 3 \le n \le 10^3 3≤n≤103(题目说 n ≥ 2 n \ge 2 n≥2但 n n n显然不可能为2), 1 ≤ a i ≤ 1 0 5 1 \le a_i \le 10^5 1≤ai≤105。
解题思路
首先,我们称那条不算边权的边为魔法边。这道题其实一上来就应该想到去枚举最小生成树的每一条边。为什么这么说?因为最小生成树上的每一条边的边权都是最小的,如果最终的边除了魔法边外不在最小生成树上,那么其边权之和一定会大于最小生成树生成的边。最小生成树足够把 n n n个点连接起来。
那么答案就呼之欲出了。首先跑出最小生成树,然后枚举每条最小生成树上的边,去掉该条边,则图会被分割成两个不连通的树。这时分别用dfs找到这两棵树上点权最大的点,用魔法边连接这两个点,更新答案。时间复杂度为 O ( n 2 ) O(n^2) O(n2),可以通过此题。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 1e3+7;
int x[N], y[N], arr[N];
double getdis(int i, int j) {
return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
}
vector<int> g[N];
set<pii> edge;
bool done[N]; // done[i] = true表示该点已经在MST中
double prim(int n) {
int s=1; // 从任意一个顶点开始
fori(1, n) done[i] = false; // 初始化
priority_queue<pair<double,pii>, vector<pair<double,pii>>, greater<pair<double,pii>>> que;
que.push({0, {s, 0}});
int cnt = 0; // 记录顶点个数而非边数!
double ans = 0; // 记录最小权值
while (que.size()) {
auto pi = que.top();
int u = pi.second.first;
que.pop();
if (done[u]) continue; // 判圈
done[u] = true;
ans += pi.first;
cnt++;
// addedge
if (pi.second.second) {
edge.insert({pi.second.second, u});
g[pi.second.second].push_back(u);
g[u].push_back(pi.second.second);
}
fori(1, n) {
if (done[i]) continue;
double dis = getdis(u, i);
que.push({dis, {i, u}});
}
}
return ans;
}
int ma = 0;
void dfs(int u, int fa) {
if (arr[u]>arr[ma]) ma = u;
for (auto v: g[u]) {
if (v == fa) continue;
dfs(v, u);
}
}
void solve() {
int n; scanf("%d", &n);
edge.clear();
fori(1, n) g[i].clear();
fori(1, n) scanf("%d%d%d", x+i, y+i, arr+i);
double res = prim(n);
double ans = 0;
for (auto pi: edge) {
int u, v;
dfs(pi.first, pi.second);
u = ma; ma = 0;
dfs(pi.second, pi.first);
v = ma; ma = 0;
ans = max(ans, 1.0*(arr[u]+arr[v])/(res-getdis(pi.first, pi.second)));
}
cout << ans << endl;
}
signed main() {
IOS;
int t; scanf("%d", &t);
cout << fixed << setprecision(2);
while (t--) {
solve();
}
return 0;
}
HDU - 2874 Connections between cities
题目大意
给你一个 n n n个节点 m m m条边的森林和 q q q次询问,每次询问给出两个点 u u u, v v v,判断两个点是否连通,如果连通则输出其最短距离。
数据范围: 2 ≤ n ≤ 1 0 4 2 \le n \le 10^4 2≤n≤104, 1 ≤ m ≤ n − 1 1 \le m \le n-1 1≤m≤n−1, 1 ≤ q ≤ 1 0 6 1 \le q \le 10^6 1≤q≤106
解题思路
非常经典的求LCA的题目,只是需要判断是否连通即可。这个也好判断,如果两个点的LCA为0,那么就不连通。
时间复杂度 O ( n log n + q log n ) O(n\log n +q\log n) O(nlogn+qlogn)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define endl '\n'
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 1e4+7;
unordered_map<ll, ll> g[N];
bool vis[N];
int dep[N], father[N];
int go[N][20];
ll dis[N][20];
void dfs(int u, int fa) {
if (vis[u]) return;
vis[u] = true;
father[u] = fa;
dep[u] = dep[fa]+1;
for (auto pi: g[u]) {
if (pi.first == fa) continue;
dfs(pi.first, u);
}
}
ll getdis(int u, int v) {
ll ans = 0;
if (dep[u]<dep[v]) swap(u, v);
for (int j=19;j>=0;--j) {
if (dep[go[u][j]]>=dep[v]) {
ans += dis[u][j];
u = go[u][j];
}
}
if (u == v) return ans;
for (int j=19;j>=0;--j) {
if (go[u][j] != go[v][j]) {
ans += dis[u][j]+dis[v][j];
u = go[u][j], v = go[v][j];
}
}
ans += dis[u][0]+dis[v][0];
if (go[u][0] == 0) return -1;
return ans;
}
signed main() {
IOS;
int n, m, q;
while (~scanf("%d%d%d", &n, &m, &q)) {
fori(1, n) {
vis[i] = false;
g[i].clear();
}
while (m--) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u].insert({v, w});
g[v].insert({u, w});
}
fori(1, n) if (!vis[i]) dfs(i, 0);
// void init
fori(1, n) {
go[i][0] = father[i];
dis[i][0] = g[i][father[i]];
}
// fori(1, n) cout << father[i] << " ";
// cout << endl;
forj(1, 19) {
fori(1, n) {
go[i][j] = go[go[i][j-1]][j-1];
dis[i][j] = dis[i][j-1]+dis[go[i][j-1]][j-1];
}
}
while (q--) {
int u, v; scanf("%d%d", &u, &v);
ll res = getdis(u, v);
if (res == -1) cout << "Not connected" << endl;
else cout << res << endl;
}
}
return 0;
}
HDU - 1217 Arbitrage
题目大意
你现在有一组货币的汇率,判断是否可以套汇。套汇的概念详见题面。
数据范围: 1 ≤ n ≤ 30 1 \le n \le 30 1≤n≤30。
解题思路
数据范围很小,可以想到floyd算法三层循环求传递闭包。但是在转移的过程中需要求的是边权的乘积而非和。
时间复杂度 O ( n 3 ) O(n^3) O(n3)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 35;
double val[N][N];
signed main() {
IOS;
int n;
int t = 0;
while (1) {
cin >> n;
if (n == 0) break;
t++;
unordered_map<string,int> maps;
fori(1, n) {
string str; cin >> str;
maps[str] = i;
}
fori(1, n) forj(1, n) val[i][j] = 0;
int m; cin >> m;
fori(1, m) {
string str1, str2; double x;
cin >> str1 >> x >> str2;
val[maps[str1]][maps[str2]] = max(val[maps[str1]][maps[str2]], x);
}
fork(1, n) fori(1, n) forj(1, n) if (val[i][k]>0&&val[k][j]>0) val[i][j] = max(val[i][j], val[i][k]*val[k][j]);
bool can = false;
cout << "Case " << t << ": ";
fori(1, n) if (val[i][i]>1+1e-10) {
can = true;
break;
}
if (can) Yes;
else No;
}
return 0;
}
HDU - 2224 The shortest path
题目大意
给你 n n n个二维平面上的点,每个点有一个坐标 x i x_i xi, y i y_i yi,且保证若 i < j i<j i<j则 x i < x j x_i<x_j xi<xj,你需要从点1出发走到点 n n n,再从点 n n n走到点1,从1走到 n n n的期间你只能往编号大的点走,从 n n n走到1期间你只能往编号小的点走。来回的过程中必须经过每个点一次,不能重复经过某个点,也不能遗漏某个点。
数据范围: 2 ≤ n ≤ 200 2 \le n \le 200 2≤n≤200。
解题思路
上网搜了搜,发现这道题目叫做双调旅行商问题。解决这道题的思路是dp。由于路径的对称性,我们把这道题转化为:从点1出发,沿两条完全不同的路径(即没有点重合)走到 n n n的最短路径之和。
设 d p i , j dp_{i,j} dpi,j表示从点1出发,第一条路径走到了点 i i i,第二条路径走到了点 j j j的最短路径长度。保证 i > j i>j i>j。显然,初始状态为 d p 2 , 1 = d i s ( 1 , 2 ) dp_{2, 1}=dis(1, 2) dp2,1=dis(1,2)。接下来让我们看如何转移:
如果 j < i − 1 j<i-1 j<i−1,那么 d p i , j = d p i − 1 , j + d i s ( i , i − 1 ) dp_{i,j}=dp_{i-1, j}+dis(i, i-1) dpi,j=dpi−1,j+dis(i,i−1),因为 i − 1 i-1 i−1走在前面,那么点 i − 1 i-1 i−1的下一个点必然是点 i i i,不能留空。
如果 j = = i − 1 j == i-1 j==i−1,那么这个时候就需要枚举了。枚举 1 ≤ k ≤ j − 1 1 \le k \le j-1 1≤k≤j−1,那么 d p i , j = min ( d p j , k + d i s ( i , k ) ) dp_{i,j}=\min (dp_{j,k}+dis(i,k)) dpi,j=min(dpj,k+dis(i,k))。为什么?因为当 j j j走到 i − 1 i-1 i−1时, i i i只能从更远的 k < j k < j k<j转移过来。而 d p k , j dp_{k, j} dpk,j和 d p j , k dp_{j, k} dpj,k是等价的,因此直接从 d p j , k dp_{j,k} dpj,k转移过来就可以了。
最后得到的答案为 d p n , n − 1 + d i s ( n − 1 , n ) dp_{n, n-1}+dis(n-1, n) dpn,n−1+dis(n−1,n)。
时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 205;
pii pos[N];
double dis(int i, int j) {
return sqrt((pos[i].first-pos[j].first)*(pos[i].first-pos[j].first)+(pos[i].second-pos[j].second)*(pos[i].second-pos[j].second));
}
double dp[N][N];
signed main() {
IOS;
int n;
cout << fixed << setprecision(2);
while (~scanf("%d", &n)) {
fori(1, n) scanf("%d%d", &pos[i].first, &pos[i].second);
dp[2][1] = dis(1, 2);
fori(3, n) forj(1, i-1) {
dp[i][j] = 1e20;
if (j == i-1) {
fork(1, j-1) {
dp[i][j] = min(dp[i][j], dp[j][k]+dis(k, i));
}
}
else dp[i][j] = dp[i-1][j]+dis(i, i-1);
}
cout << (dp[n][n-1]+dis(n, n-1)) << endl;
}
return 0;
}
HDU - 1142 A Walk Through the Forest
题目大意
给你一张 n n n个点 m m m条边的无向图, 1 1 1为起点, 2 2 2为终点,从1号节点出发,当经过某个点 u u u时,如果存在一个点 v v v,使得 u , v u,v u,v相连,且 v v v到2的最短路径比 u u u到2的最短路径要短,那么就可以从 u u u走到 v v v。计算从1到2所有不同的路径数量。
数据范围: 2 ≤ n ≤ 1 0 3 2 \le n \le 10^3 2≤n≤103,边权 1 ≤ d ≤ 1 0 6 1 \le d \le 10^6 1≤d≤106。
解题思路
这道题是一道简单而又巧妙的题目。首先我们需要知道对于两个相邻节点 u u u和 v v v,怎么判断 u u u是否能够走向 v v v。很简单,从终点2跑一次单源最短路,然后就可以判断了。
那么如何计数?这里我的方法是dfs+记忆化搜索。我从节点1出发,逐一递归其邻接点,并将其所有邻接点的答案求和,更新该节点的dp值,再下一次搜索的时候直接使用该节点的dp值即可。
时间复杂度 O ( m log n + n ) O(m\log n+n) O(mlogn+n)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 1e3+7;
vector<pii> g[N];
int dist[N];
char vis[N];
void Dijkstra(int s, int n) {
fori(1, n) dist[i] = -1, vis[i] = 0;
dist[s] = 0;
priority_queue<pii, vector<pii>, greater<pii>> pri; // 优先队列
pri.push({dist[s],s});
while (pri.size()) {
pii pi = pri.top();
pri.pop();
if (vis[pi.ss]) continue;
vis[pi.ss] = 1;
dist[pi.ss] = pi.ff;
for (int i=0;i<g[pi.ss].size();++i) {
pri.push({pi.ff+g[pi.ss][i].ss, g[pi.ss][i].ff});
}
}
}
int dp[N];
int dfs(int u) {
if (dp[u]) return dp[u];
if (u == 2) return 1;
int res = 0;
for (auto pi: g[u]) {
if (dist[pi.first]>=dist[u]) continue;
res += dfs(pi.first);
}
return (dp[u] = res);
}
signed main() {
IOS;
int n, m;
while (~scanf("%d", &n)&&n) {
scanf("%d", &m);
fori(1, n) g[i].clear();
while (m--) {
int u,v,w;
scanf("%d%d%d", &u, &v, &w);
g[u].push_back({v, w});
g[v].push_back({u, w});
}
Dijkstra(2, n);
fori(1, n) dp[i] = 0;
cout << dfs(1) << endl;
}
return 0;
}
HDU - 3639 Hawk-and-Chicken
题目大意
班里有 n n n个同学,全班要开一次班会选举班长,每个同学都可以“支持”其他同学,这种支持关系是具有传递性的,若同学A支持同学B,同学B支持同学C,那么同学A也会支持同学C。但自己不能支持自己。给定 m m m对支持关系,找到受到最多人支持的若干个同学,并输出这些同学的编号和支持他们的人数。
数据范围: 2 ≤ n ≤ 5 × 1 0 3 2 \le n \le 5\times 10^3 2≤n≤5×103, 1 ≤ m ≤ 3 × 1 0 4 1 \le m \le 3\times 10^4 1≤m≤3×104。
解题思路
看到这道题和样例,就可以想到:如果 k k k个同学互相支持,那么这 k k k个同学的支持数都为 k − 1 k-1 k−1。这里可以考虑使用Tarjan求强连通分量,然后缩点,得到一张DAG。接下来关键步骤来了:如何计算出支持数最多的同学呢?也就是如何在DAG上找到前驱数量最多的点。我当时想了半天,但是没有什么好的思路,因为无法用拓扑排序求出不重复的支持数。但考虑到 n n n的范围较小,或许可以通过减小常数通过 O ( n 2 ) O(n^2) O(n2)的算法。
考虑一张DAG,前驱数量最多的点只可能是出度为0的点。但直接在这张图上搜答案显然不太现实,因此我们考虑在缩点时建反图,然后求出后继数量最多的点。但这里只能用暴力了,从每一个入度为0的点开始跑dfs。复杂度是 O ( n 2 ) O(n^2) O(n2),但常数 ≤ 1 4 \le \frac{1}{4} ≤41,可以通过此题。最后把答案记录下来输出即可。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 5e3+7;
vector<int> g[N];
int scc[N], low[N], num[N]; // scc代表强连通分量的序号,low代表它所连接的最小祖先的序号,num代表访问到它时的序号
int dfn, cnt; // dfn为访问序号,cnt记录强连通分量的个数和编号
stack<int> st; // 用于保存属于同一个强连通分量的点
void dfs(int u) {
st.push(u);
low[u] = num[u] = ++dfn; // 初始化
fori(0, g[u].size()-1) {
int v = g[u][i];
if (!num[v]) { // 如果没有访问过
dfs(v);
low[u] = min(low[u], low[v]); // 递归返回子节点的low
}
else if (!scc[v]) { // 如果子节点v访问过且不属于某一个强连通分量(好像直接else不加判断也可以)
low[u] = min(low[u], num[v]); // 该点的序号
}
}
if (low[u] == num[u]) { // 栈底:scc祖先
cnt++; // 这是一个新的强连通分量
while (1) {
int v = st.top();
st.pop();
scc[v] = cnt; // 记录编号
if (v == u) break; // 栈底为该强连通分量的祖先节点
}
}
}
int siz[N], dp[N], deg[N];
vector<int> gg[N];
set<int> tmp;
void dfs2(int u) {
tmp.insert(u);
for (auto v: gg[u]) {
if (tmp.find(v) != tmp.end()) continue;
dfs2(v);
}
}
void solve(int tt) {
int n, m;
cin >> n >> m;
dfn = cnt = 0;
fori(1, n) {
g[i].clear();
gg[i].clear();
scc[i] = low[i] = num[i] = 0;
siz[i] = dp[i] = deg[i] = 0;
}
fori(1, m) {
int u, v;
cin >> u >> v;
u++, v++;
g[u].push_back(v);
}
fori(1, n) if (!num[i]) dfs(i);
fori(1, n) {
siz[scc[i]]++;
for (auto v: g[i]) {
if (scc[v] == scc[i]) continue;
gg[scc[v]].push_back(scc[i]); // inverse
deg[scc[i]]++;
}
}
fori(1, cnt) {
if (deg[i]) continue;
tmp.clear();
dfs2(i);
int res = 0;
for (auto u: tmp) res += siz[u];
dp[i] = res;
}
set<int> sets;
int ma = 0;
fori(1, cnt) {
if (dp[i]>ma) {
ma = dp[i];
sets.clear();
sets.insert(i);
}
else if (dp[i] == ma) {
sets.insert(i);
}
}
vector<int> ans;
fori(1, n) if (sets.find(scc[i]) != sets.end()) ans.push_back(i);
sort(ans.begin(), ans.end());
cout << "Case " << tt << ": " << ma-1 << endl;
for (auto u: ans) cout << u-1 << " ";
cout << endl;
}
signed main() {
IOS;
int t;
cin >> t;
fori(1, t) {
solve(i);
}
return 0;
}