文章目录
- 写在前面
- [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 - 2767 Proving Equivalences](https://vjudge.net/problem/HDU-2767) 和 [HDU - 3836 Equivalent Sets ](https://vjudge.net/problem/HDU-3836)
- [HDU - 3862 考研路茫茫——空调教室](https://vjudge.net/problem/HDU-2242)
写在前面
前段时间做多校,感觉自己的基础不太好,之前学过的一些东西也都忘记了,想趁这个机会回来补一补基础,尤其是图论这一块。因此我打算回来做一些经典的图论算法题,“不积跬步,无以至千里”,还是从基础的开始,一步一步去挑战更难的题目。
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;
}
HDU - 2767 Proving Equivalences 和 HDU - 3836 Equivalent Sets
题目大意
给你了 n n n个命题,你需要证明这 n n n个命题是等价的,其中每一步你可以证明任意两个命题 a → b a \rightarrow b a→b,且你现在已经证明了 m m m个蕴含关系,求问你最少还需要多少步才能证明这 n n n个命题等价。
解题思路
这道题的核心思想首先是把问题转化为有向图,其中
a
→
b
a \rightarrow b
a→b代表从节点
a
a
a引一条边到节点
b
b
b。接下来对该图求强连通分量后缩点,构造一张DAG,接下来就要通过这一张DAG来求解最少所需的证明数量了。
考虑一张DAG,它有若干个入点和出点,如果要让这整一个DAG形成一个大的强连通分量,那么每一个入点和出点都至少要额外加一条边连向其他点。为什么?因为如果入点不连边,那么就没有其他命题能证明它;如果出点不连边,那么它就不能证明其他命题。那么什么情况下最优呢?答案就是从出点连一条边到入点。为什么这种情况下最优?因为如果从出点连一条边到其它点,那么剩下的那个入点还需要从其它点再连一条边到它自己。因此直接将各个出点和入点相连是最好的。因此答案就是
max
(
入点个数
,
出点个数
)
\max (入点个数, 出点个数)
max(入点个数,出点个数)。
注意:特判缩点后只剩一个点的情况:说明已经证明完毕了,输出0。
AC代码
PS:2767和3836的输入格式略有不同,但题目大意相同。下面的代码是2767的。
#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 = 2e4+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 in[N], out[N];
void solve() {
int n, m;
cin >> n >> m;
fori(1, n) {
g[i].clear();
in[i] = out[i] = 0;
num[i] = low[i] = scc[i] = 0;
}
dfn = 0, cnt = 0;
fori(1, m) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
}
fori(1, n) if (!num[i]) dfs(i);
assert(st.empty());
set<int> sets;
fori(1, n) sets.insert(scc[i]);
if (sets.size() == 1) {
cout << 0 << endl;
return;
}
fori(1, n) {
for (auto v: g[i]) {
if (scc[v] == scc[i]) continue;
out[scc[i]]++; in[scc[v]]++;
}
}
int inn = 0, oout = 0;
fori(1, cnt) {
if (in[i] == 0) inn++;
if (out[i] == 0) oout++;
}
cout << max(inn, oout) << endl;
}
signed main() {
IOS;
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
HDU - 3862 考研路茫茫——空调教室
题目大意
给你一个 n n n个节点的无向图,每个节点有一个点权 a i a_i ai,要求出删掉某一条边后,图被分为两部分的点权之和差值绝对值的最小值。输出最小值。如果不存在一条边,使得删除这条边后可以将图分为两个不相连的连通块,则输出impossible。
解题思路
这道题用到了边双连通分量缩点。求出这张图的边双连通分量,然后缩点,可以得到一棵树。这棵树每个节点的点权就是缩点前的图缩成这个点的所有节点点权之和。得到一棵树后就好办了,直接做树形dp即可求出最小值。
特判边双连通分量为1的情况,此时输出impossible。
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;}
inline char nc() {
static char buf[100000],*l=buf,*r=buf;
return l==r&&(r=(l=buf)+fread(buf,1,100000,stdin),l==r)?(exit(0), EOF):*l++;
}
template <class T = int>
inline T rd() {
T res=0,f=1;char ch=nc();
while (ch<'0'||'9'<ch) {if (ch=='-') f=-f;ch=nc();}
while ('0'<=ch&&ch<='9') res=res*10+ch-48,ch=nc();
return res*f;
}
inline bool chvalid(char ch) {
// isalpha:是否是字母
// isdigit:是否是数字
return isalnum(ch);
}
// 读入字符
inline char rdch() {
char ch=nc();while (!chvalid(ch)) ch=nc();
return ch;
}
inline string rdstr() {
string p = "";
char ch = nc(); while (!chvalid(ch)) ch=nc();
p+=ch;
while(ch = nc()) if (chvalid(ch)) {p+=ch;} else break;
return p;
}
// 可以顺带着返回字符串长度
inline string rdstr(int &len) {
string p = rdstr();
len = p.size();
return p;
}
const int N = 1e5+7;
int head[N], ecnt;
struct {
int from, to, next;
} edge[N<<1];
void adde(int u, int v) {
edge[ecnt].from = u;
edge[ecnt].to = v;
edge[ecnt].next = head[u];
head[u] = ecnt++;
}
void addedge(int u, int v) {
adde(u, v);
adde(v, u);
}
int dfn = 0, cnt = 0; // dfs序号
int num[N], low[N], bcc[N]; // dfs序和深度
bool vis[N]; // 是否访问过
stack<int> st;
void dfs(int u, int id) {
low[u] = num[u] = ++dfn;
st.push(u), vis[u] = 1;
for (int i=head[u]; ~i; i=edge[i].next) {
if ((i^1) == id) continue; // 判断父边
int v = edge[i].to;
if (!num[v]) {
dfs(v, i);
low[u] = min(low[u], low[v]);
}
else if (vis[v]) low[u] = min(low[u], num[v]);
}
if (num[u] == low[u]) { // 回到顶层
cnt++;
while (1) {
int v = st.top();
vis[v] = 0;
bcc[v] = cnt;
st.pop();
if (u == v) break;
}
}
}
int arr[N];
vector<int> g[N];
int val[N];
int dp[N];
int mi = 1e9, sum = 0;
void dfs2(int u, int fa) {
dp[u] = val[u];
for (auto v: g[u]) {
if (v == fa) continue;
dfs2(v, u);
dp[u] += dp[v];
}
mi = min(mi, abs(sum-2*dp[u]));
}
void solve() {
int n = rd(), m = rd();
// init
ecnt = cnt = dfn = 0;
fori(1, n) {
head[i] = -1;
num[i] = low[i] = bcc[i] = 0;
val[i] = 0;
g[i].clear();
}
fori(1, n) arr[i] = rd();
fori(1, m) {
int u = rd(), v = rd();
u++, v++;
addedge(u, v);
}
fori(1, n) if (!num[i]) dfs(i, -1);
set<int> sets;
fori(1, n) sets.insert(bcc[i]);
if (sets.size() == 1) {
cout << "impossible" << endl;
return;
}
forj(1, n) {
val[bcc[j]] += arr[j];
for (int i=head[j];~i;i=edge[i].next) {
int v = edge[i].to;
if (bcc[v] == bcc[j]) continue;
g[bcc[j]].push_back(bcc[v]);
}
}
mi = 1e9, sum = 0;
fori(1, cnt) sum += val[i];
dfs2(1, 0);
cout << mi << endl;
}
signed main() {
IOS;
while (1) {
solve();
}
return 0;
}