P10480 可達性統計(拓撲,bitset 最佳化)

Zhang_Wenjie發表於2024-07-23

link

從數的角度來看,如果知道任意一個點能到達的點的數量,那麼它的前驅節點一定也能到達,但是,只累加數的話無法處理可能存在重合點的情況。

所以,考慮從集合的角度,設 \(f(x)\) 表示 \(x\) 能到達的點的集合

如果 \(x\) 有鄰點 \(y_1,y_2,...,y_k\),那麼 \(x\) 能到達的點就是它的鄰點能到達的點的並集

\[f(x) = \{x\} \cup f(y_1) \cup f(y_2) ... \cup f(y_k) \]

在 DAG 裡邊,我們可以先跑個拓撲序出來,從後往前處理 \(f(x)\),這樣沒有後效性

時間複雜度呢 ... 我覺得可以從每個點的貢獻角度想,

訪問集合中的每個點時間貢獻為 \(O(1)\),考慮極限情況:1 可以到達 2 ~ n,2 可以達到 3 ~ n ... 那麼每訪問完一個 \(f(x)\),貢獻分別為 n - 1,n - 2 ... 2,1

累加起來就是 \(O(\frac{n(n-1)}{2})\),比拓撲排序高,瓶頸,且不能接受


考慮位運算最佳化,也就是狀態壓縮的思想

這裡有個很冷門?的工具 bitset<N>,表示一個 N 位的二進位制數,每八位佔用一個位元組

而我們知道一個 int 變數是有 \(2^{32} - 1\) 的最大值,也就是說一個 int 可以存 \(w = 32\) 位二進位制數

那麼 N 位的 bitset 執行一次位運算的複雜度就為 \(O(\frac{N}{w})\)

所以所以,每個集合 \(f(x)\) 假設有 n 個數,狀態壓縮完就是一個 n 位的二進位制數,再用 bitset 壓縮,每 32 位壓縮為 1 位,那麼總的複雜度就降到 \(O(\frac{n(n-1)}{2w})\),大概 1.5e7 的樣子

#include <bits/stdc++.h>
#define re register int 

using namespace std;
const int N = 3e4 + 10;

struct Edge
{
	int to, next;
}e[N];
int top, h[N], in[N];
int n, m;

vector<int> seq;
bitset<N> f[N];

inline void add(int x, int y)
{
	e[++ top] = (Edge){y, h[x]};
	h[x] = top;
}

inline void topsort()
{
	queue<int> q;
	for (re i = 1; i <= n; i ++)
		if (in[i] == 0) q.push(i);
		
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		
		seq.push_back(x);
		for (re i = h[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			if (-- in[y] == 0)
				q.push(y);
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	while (m --)
	{
		int x, y; cin >> x >> y;
		
		add(x, y);
		in[y] ++;
	}
	topsort();

	for (re i = seq.size() - 1; i >= 0; i --)
	{
		int u = seq[i];
		
		f[u][u] = 1;
		for (re j = h[u]; j; j = e[j].next)
		{
			int v = e[j].to;
			f[u] |= f[v];
		}
	}
	for (re i = 1; i <= n; i ++) 
		cout << f[i].count() << '\n'; //bitset成員函式,返回有多少位是 1
		
	return 0;
}

相關文章