题目链接:https://codeforces.com/contest/1547/problem/G
所用算法:
题意:
给定一个由 n 个点构成的有向有环图,判断 1 号点到 1~n 每个点的可达路径数目,对于点 \(i(1\le i\le n)\),若点 1 到点 i 之间没有路径,输出 0,若有且仅有一条路径,输出 1,若有多条路径,输出 2,若有无限条路径,输出-1。
t 组测试样例,每次输入 n 个点 m 条有向边,输出 n 个数分别表示点 1 到点 1~n 的可达情况。
思路:
容易看出若 1 号点到某个节点的路径有多条或无限多条,则 1 号点到这个节点的所有可达节点的路径就至少有同样多条或无限多条。
因此有两种思路:
-
强连通分量缩点+拓扑排序 dp
- 由于有向图中包含的点的数目大于 1 的强连通分量中一定有环,因此对于一个点数目大于 1 强连通分量,只要 1 号点可达该强连通分量,则 1 号点到该强连通分量内的所有点以及该强连通分量可达的所有强连通分量内的点的路径数都为无穷多
- 另外若某个点数目为 1 的强连通分量中的那个点存在自环,则其也满足此性质
- 然后拓扑排序 +dp 统计路径数目,对上述两种情况特殊判断即可
-
直接从 1 号点开始 dfs,若当前点正在被访问时再次被访问到,则存在环,1 号点到它以及它所有的可达点的路径数目均为无穷大,若访问到已经被访问过的点,则说明存在至少两条路径到达该被访问的点以及其所有可达点,因此可以直接 dfs 然后 按优先级(无穷多条>有限多条>一条>不可达) 对 1 号点到每个节点的路径数进行更新即可(或者可以将这个过程拆成两次 dfs,第一次找可达节点,第二次找路径为有限多条和无限多条的节点,本质是一样的)
要注意的是如果用 vector 存图需要每次重新申请内存,如果用 clear 的话会超时
代码:
代码 1:
//强连通分量缩点+拓扑排序dp
#include <iostream>
#include <vector>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
const int maxn = 4e5 + 5, maxm = 4e5 + 5;
vector<vector<int>> edges; //存原图的边
vector<vector<int>> edges_scc; //存强连通分量缩点之后的边
stack<int> stk;
vector<int> dfn; //dfn[u]:u点的dfs序
vector<int> low; // low[u]:u点经过最多一条非树边可达的最小dfs序结点的dfs序
vector<int> idx; // idx:每个点所属的强连通分量编号
vector<bool> instk; //instk:是否在栈中
int dfsn, cnt_scc; //dfsn:当前点的dfs序,cnt_scc:强连通分量的数量
vector<bool> isloop, isloop_scc; //标记点是否有自环和强连通分量内是否有环
vector<int> deg; //degree,存每个结点的入度,在存图的时候需要录入数据
vector<int> dp; //dp[i]表示第i个强连通分量内的点的路径数
void tarjan(int u)
{
low[u] = dfn[u] = ++dfsn; //按dfs序赋值dfn以及low(每个结点至少可达自己)
instk[u] = 1;
stk.push(u); // 进栈
for (auto v : edges[u]) //遍历当前点的所有边
{
if (!dfn[v]) //未访问过v,则为v为u的儿子结点
{
tarjan(v); //先递归儿子结点
low[u] = min(low[u], low[v]); //用儿子结点的low值更新父亲结点的low值
}
else if (instk[v]) //已经访问过v,且v在栈中,即u可达v
low[u] = min(low[u], dfn[v]); //用v的dfs序的值更新u的low值
}
if (low[u] == dfn[u]) //若当前节点的low值计算完后dfn值等于low值,则当前节点为一个强连通分量的根结点
{
int top;
cnt_scc++; //强连通分量数量+1
int num = 0; //统计强连通分量内点的个数
do
{
num++;
top = stk.top();
stk.pop(); //取栈首结点并弹栈
instk[top] = 0; //标记此节点已不在栈中
idx[top] = cnt_scc; // 记录所属的强连通分量
if (isloop[top] || num > 1) //若当前点有自环或者当前强连通分量内点的个数大于1,则强连通分量内有环
isloop_scc[cnt_scc] = 1;
} while (top != u); //直到弹出u才停止
}
}
void findscc(int n)
{
for (int i = 1; i <= n; ++i) //tarjan求强连通分量
if (!dfn[i])
tarjan(i);
for (int u = 1; u <= n; ++u) //遍历所有点 //强连通分量缩点
for (auto v : edges[u]) //遍历当前点的所有边
if (idx[u] != idx[v]) //若当前点与其所连接的点不属于同一强连通分量,则当前边为缩点之后的一条边
{
edges_scc[idx[u]].push_back(idx[v]);
deg[idx[v]]++; //统计强连通分量缩点后的入度
}
}
void toposort(int n)
{
queue<int> q;
for (int i = 1; i <= n; ++i) //先找到入度为0的点
{
if (deg[i] == 0)
q.push(i); //加入队列
}
while (!q.empty())
{
int u = q.front();
q.pop();
for (auto v : edges_scc[u])
{ //遍历入度为0的点的边
deg[v]--; //更新入度
if (deg[v] == 0) //若出现了新的入度为0的点
q.push(v); //加入队列
if (dp[u] != 0) //若当前强连通分量从点1不可达,则跳过
{
if (dp[u] == -1 || isloop_scc[v]) //若当前强连通分量路径数为无穷大或下一个强连通分量内有环
dp[v] = -1;
else if (dp[v] != -1)
{
if (dp[u] == 2 || dp[v]) //若当前强连通分量路径数有多条或下一个强连通分量已经有至少一条路径
dp[v] = 2;
else //若当前强连通分量路径只有一条且下一个强连通分量路径数为0
dp[v] = 1;
}
}
}
}
}
void init(int n)
{
edges = vector<vector<int>>(n + 5);
edges_scc = vector<vector<int>>(n + 5);
stk = stack<int>();
dfn = vector<int>(n + 5);
low = vector<int>(n + 5);
idx = vector<int>(n + 5);
instk = vector<bool>(n + 5);
cnt_scc = 0, dfsn = 0;
isloop = vector<bool>(n + 5);
isloop_scc = vector<bool>(n + 5);
deg = vector<int>(n + 5);
dp = vector<int>(n + 5);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int t, n, m;
cin >> t;
while (t--)
{
int u, v;
cin >> n >> m;
init(n);
for (int i = 0; i < m; i++)
{
cin >> u >> v;
edges[u].push_back(v);
if (v == u) //有自己指向自己的边,则有自环
isloop[v] = 1;
}
findscc(n);
dp[idx[1]] = (isloop_scc[idx[1]] ? -1 : 1); //1号点所在的强连通分量的路径数预先给定
toposort(cnt_scc);
for (int i = 1; i <= n; i++)
cout << dp[idx[i]] << " ";
cout << endl;
}
}
代码 2:
//直接一次dfs统计答案
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int maxn = 4e5 + 5;
vector<vector<int>> g;
vector<int> ans;
vector<bool> vis;
void init(int n)
{
n++;
g = vector<vector<int>>(n);
ans = vector<int>(n);
vis = vector<bool>(n);
}
void dfs(int now) //答案更新优先级:-1>2>1>0,即无穷多条>有限且多条>一条>不可达
{
vis[now] = 1; //标记当前节点正在被访问
for (auto nex : g[now]) //遍历可达节点,然后依次判断优先级
{
if (ans[nex] == -1) //若当前节点路径数已经为无穷大,则不再dfs
continue;
else if (vis[nex] == 1 || ans[now] == -1)
{ //下一个节点正在被访问,形成了环,或者当前节点的祖先节点已经形成了环,则其能访问到的所有节点的路径数为无穷大
ans[nex] = -1;
}
else if (ans[nex] == 2) //若当前节点路径数为多条,也不再dfs
continue;
else if (ans[nex] == 1 || ans[now] == 2)
{ //下一个节点已经被访问过或其祖先节点路径有多条,则当前节点的路径有多条,其能访问到的节点的路径数至少有两条
ans[nex] = 2;
}
else //只要能被dfs到,则其路径至少一条
ans[nex] = 1;
dfs(nex); //若当前节点的答案刚被更新,则dfs
}
vis[now] = 0;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int t, n, m;
cin >> t;
while (t--)
{
cin >> n >> m;
init(n);
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
}
ans[1] = 1;
dfs(1);
for (int i = 1; i <= n; i++)
cout << ans[i] << " ";
cout << endl;
}
}
代码 3:
//拆成两次dfs写
#include <iostream>
#include <vector>
#include <cstring>
#include <set>
using namespace std;
const int maxn = 4e5 + 5;
vector<vector<int>> g;
vector<int> ans;
vector<bool> vis;
set<int> res[2];
void init(int n)
{
n++;
g = vector<vector<int>>(n);
ans = vector<int>(n);
vis = vector<bool>(n);
for (int i = 0; i < 2; i++)
res[i] = set<int>();
}
void dfs(int now)
{
vis[now] = 1; //标记当前节点正在被访问
ans[now] = 1;
for (auto nex : g[now])
{
if (ans[nex] == 0)
dfs(nex);
else if (vis[nex])
res[1].insert(nex);
else
res[0].insert(nex);
}
vis[now] = 0;
}
void dfs2(int now)
{
for (auto nex : g[now])
{
if (ans[nex] != ans[now])
{
ans[nex] = ans[now];
dfs2(nex);
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int t, n, m;
cin >> t;
while (t--)
{
cin >> n >> m;
init(n);
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
}
dfs(1);
for (int i = 0; i < 2; i++)
{
for (auto j : res[i])
{
i == 0 ? ans[j] = 2 : ans[j] = -1;
dfs2(j);
}
}
for (int i = 1; i <= n; i++)
cout << ans[i] << " ";
cout << endl;
}
}
总结:
- 使用 STL 容器时,若要多次重复使用且每次需要重新初始化,最好每次直接重新申请空间,使用 clear 函数时间消耗十分巨大