JZOJ 幽幽子与森林

题目大意:

迷途竹林可以看成是一个n个点的森林,幽幽子定义dis(u,v)为u到v路径上的边的数量,若u和v不连通则为m。她定义整个森林的危险度为
为了去拜访永琳师匠,幽幽子需要提前知道迷途竹林的危险度。但迷途竹林的形态是时刻变化着的,所以幽幽子希望知道危险度的期望是多少。
为了避免浮点运算,答案对998244353取模。
1<=n<=2e5

题解:

这个计数写得真的是累。

很容易想到设:
f[i]f[i]f[i]表示i个点生成树个数
g[i]g[i]g[i]表示i个点生成森林计数
c[i]c[i]c[i]表示i个点生成树的距离平方和。

只要能求出这三个东西,再随便卷卷就能搞出来答案了。

f[0]=f[1]=1,f[i]=ii−2(i&gt;1)f[0]=f[1]=1,f[i]=i^{i-2}(i&gt;1)f[0]=f[1]=1,f[i]=ii2(i>1)
g=efg=e^fg=ef,一些阶乘忽略掉了

copy个版还算轻松。

然后就是求c。

思路为距离平方和拆为中间的有序点对数*2-点的个数+1。

那么考虑枚举这两个有序点对,大概是三个数组卷起来。

再枚举点,大概是两个数组卷起来。

然后就愉快的解决了这题。

Code:

#include<vector>
#include<cstdio>
#include<algorithm>
#define ll long long
#define pp printf
#define fo(i, x, y) for(int i = x, B = y; i <= B; i ++)
#define ff(i, x, y) for(int i = x, B = y; i <  B; i ++)
#define fd(i, x, y) for(int i = x, B = y; i >= B; i --)
#define pb push_back
using namespace std;

typedef vector<ll> V;

const int N = (1 << 21) + 5;

const int mo = 998244353;

ll ksm(ll x, ll y) {
    ll s = 1;
    for(; y; y /= 2, x = x * x % mo)
        if(y & 1) s = s * x % mo;
    return s;
}

int r[N]; ll aa[N];

void dft(V &b, int f) {
	int n = b.size();
    ff(i, 0, n) aa[i] = b[i];
    ff(i, 0, n) {
        r[i] = r[i / 2] / 2 + (i & 1) * (n / 2);
        if(i < r[i]) swap(aa[i], aa[r[i]]);
    }
    for(int h = 1; h < n; h *= 2) {
        ll wn = ksm(ksm(3, (mo - 1) / 2 / h), f == -1 ? mo - 2 : 1);
        for(int j = 0; j < n; j += 2 * h) {
            ll A, W = 1, *l = aa + j, *r = aa + j + h;
            ff(i, 0, h) {
                A = *r * W, *r = (*l - A) % mo, *l = (*l + A) % mo;
                W = W * wn % mo; l ++; r ++;
            }
        }
    }
    if(f == -1) {
        ll v = ksm(n, mo - 2);
        ff(i, 0, n) aa[i] = (aa[i] + mo) * v % mo;
    }
    ff(i, 0, n) b[i] = aa[i];
}

V operator * (V a, V b) {
    int z = a.size() + b.size() - 1;
    int n = 1; while(n < z) n *= 2;
    a.resize(n); b.resize(n);
    dft(a, 1); dft(b, 1);
    ff(i, 0, n) a[i] = a[i] * b[i] % mo;
    dft(a, -1); a.resize(z); return a;
}

V operator - (V a, V b) {
	if(a.size() < b.size()) a.resize(b.size());
	ff(i, 0, a.size()) a[i] = (a[i] - b[i] + mo) % mo;
	return a;
}

V qni(V a) {
    int n0 = 1; while(n0 < a.size()) n0 *= 2;
    V b; b.clear(); b.pb(ksm(a[0], mo - 2));
    for(int n = 2; n <= n0; n *= 2) {
        b.resize(n * 2); dft(b, 1);
        V c = a; c.resize(n); c.resize(n * 2); dft(c, 1);
        ff(i, 0, n * 2) b[i] = (b[i] * 2 - b[i] * b[i] % mo * c[i]) % mo;
        dft(b, -1); b.resize(n);
    }
    b.resize(a.size()); return b;
}

V qd(V a) {
    a[0] = 0;
    ff(i, 1, a.size()) a[i - 1] = a[i] * i % mo;
    return a;
}

V jf(V a) {
    a.pb(0);
    fd(i, a.size(), 1) a[i] = a[i - 1] * ksm(i, mo - 2) % mo;
    a[0] = 0;
    return a;
}

V ln(V a) {
    int sa = a.size();
    V b = a; b = qni(b); a = qd(a);
    a = a * b; a = jf(a); a.resize(sa);
    return a;
}

V exp(V a) {
    int n0 = 1; while(n0 < a.size()) n0 *= 2;
    V b; b.clear(); b.pb(1);
    for(int n = 2; n <= n0; n *= 2) {
		V c = b; c.resize(n); c = ln(c);
		V d = a; d.resize(n);
		c = c - d;
		c.resize(2 * n); dft(c, 1);
		b.resize(2 * n); dft(b, 1);
		ff(i, 0, 2 * n) b[i] = (b[i] - c[i] * b[i]) % mo;
		dft(b, -1); b.resize(n);
	}
	b.resize(a.size());
	return b;
}

int T, n, m;
ll fac[N], nf[N], f[N];
int n0;

V a, b;

ll c[N], d[N], ans1[N], ans2[N];

ll ni2;

int main() { 
	n = 2e5;
	fac[0] = 1; fo(i, 1, n) fac[i] = fac[i - 1] * i % mo;
	nf[n] = ksm(fac[n], mo - 2); fd(i, n, 1) nf[i - 1] = nf[i] * i % mo;
	f[0] = f[1] = 1; fo(i, 2, n) f[i] = ksm(i, i - 2);
	n0 = 1; while(n0 <= n) n0 *= 2;
	
	a.resize(4 * n0); b.resize(4 * n0);
	fo(i, 1, n) a[i] = b[i] = f[i] * i % mo * i % mo * nf[i] % mo;
	b[0] = 1;
	
	dft(a, 1); dft(b, 1);
	ff(i, 0, a.size()) a[i] = a[i] * a[i] % mo * b[i] % mo;
	dft(a, -1);
	
	fo(i, 1, n) c[i] = a[i] * fac[i] * 2 % mo;
	a.clear(); a.resize(2 * n0);
	b.clear(); b.resize(2 * n0);
	fo(i, 1, n) a[i] = f[i] * i % mo * i % mo * nf[i] % mo;
	dft(a, 1);
	ff(i, 0, a.size()) a[i] = a[i] * a[i] % mo;
	dft(a, -1);
	fo(i, 1, n) c[i] = (c[i] - a[i] * fac[i]) % mo;
	
	a.clear(); a.resize(n + 1);
	fo(i, 1, n) a[i] = f[i] * nf[i] % mo;
	a = exp(a);
	fo(i, 0, n) d[i] = a[i] * fac[i] % mo;
	
	a.clear(); a.resize(n + 1);
	fo(i, 1, n) a[i] = c[i] * nf[i] % mo;
	b.clear(); b.resize(n + 1);
	fo(i, 0, n) b[i] = d[i] * nf[i] % mo;
	a = a * b;
	fo(i, 1, n) ans1[i] = a[i] * fac[i] % mo;
	
	a.clear(); a.resize(n + 1);
	fo(i, 1, n) a[i] = f[i] * nf[i] % mo * i % mo;
	b.clear(); b.resize(n + 1);
	fo(i, 1, n) b[i] = d[i] * nf[i] % mo * i % mo;
	a = a * b;
	fo(i, 1, n) ans2[i] = a[i] * fac[i] % mo;
	
	ni2 = ksm(2, mo - 2);
	for(scanf("%d", &T); T; T --) {
		scanf("%d %d", &n, &m);
		pp("%lld\n", (ans1[n] + ans2[n] * m % mo * m) % mo * ksm(d[n], mo - 2) % mo * ni2 % mo);
	}
}
### 解题思路 题目要求解决的是一个图相关的最小覆盖问题,通常在特定条件下可以通过状态压缩动态规划(State Compression Dynamic Programming, SCDP)来高效求解。由于状态压缩的适用条件是状态维度较小(例如K≤10),因此可以利用二进制表示状态集合,从而优化计算过程。 #### 1. 状态表示 - 使用一个整数 `mask` 表示当前选择的点集,其中第 `i` 位为 `1` 表示第 `i` 个节点被选中。 - 定义 `dp[mask]` 表示在选中 `mask` 所代表的点集后,能够覆盖的节点集合。 - 可以通过预处理每个点的邻域信息(包括自身和所有直接连接的点),快速更新状态。 #### 2. 预处理邻域 对于每个节点 `u`,预先计算其邻域范围 `neighbor[u]`,即从该节点出发一步能到达的所有节点集合。这样,在后续的状态转移过程中,可以直接使用这些信息进行合并操作。 #### 3. 状态转移 - 初始化:对每个单独节点 `u`,设置初始状态 `dp[1 << u] = neighbor[u]`。 - 转移规则:对于任意两个状态 `mask1` 和 `mask2`,如果它们没有交集,则可以通过合并这两个状态得到新的状态 `mask = mask1 | mask2`,并更新对应的覆盖范围为 `dp[mask1] ∪ dp[mask2]`。 - 在所有状态生成之后,检查是否某个状态的覆盖范围等于全集(即覆盖了所有节点)。如果是,则记录此时使用的最少节点数量。 #### 4. 最优解提取 遍历所有可能的状态,找出能够覆盖整个图的最小节点数目。 --- ### 时间复杂度分析 - 状态总数为 $ O(2^K) $,其中 `K` 是关键点的数量。 - 每次状态转移需要枚举所有可能的集组合,复杂度为 $ O(2^K \cdot K^2) $。 - 整体时间复杂度控制在可接受范围内,适用于 `K ≤ 10~20` 的情况。 --- ### 代码实现(状态压缩 DP) ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 25; int neighbor[MAXN]; // 每个节点的邻域 int dp[1 << 20]; // dp[mask] 表示选中的点集合为 mask 时所能覆盖的点集合 int min_nodes; // 最小覆盖点数 void solve(int n, vector<vector<int>>& graph) { // 预处理每个节点的邻域 for (int i = 0; i < n; ++i) { neighbor[i] = (1 << i); // 包括自己 for (int j : graph[i]) { neighbor[i] |= (1 << j); } } // 初始化 dp 数组 memset(dp, 0x3f, sizeof(dp)); for (int i = 0; i < n; ++i) { dp[1 << i] = neighbor[i]; } // 状态转移 for (int mask = 1; mask < (1 << n); ++mask) { if (__builtin_popcount(mask) >= min_nodes) continue; // 剪枝 for (int sub = mask & (mask - 1); sub; sub = (sub - 1) & mask) { int comp = mask ^ sub; if (comp == 0) continue; int new_mask = mask; int covered = dp[sub] | dp[comp]; if (covered == (1 << n) - 1) { min_nodes = min(min_nodes, __builtin_popcount(new_mask)); } dp[new_mask] = min(dp[new_mask], covered); } } } int main() { int n, m; cin >> n >> m; vector<vector<int>> graph(n); for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; graph[u].push_back(v); graph[v].push_back(u); // 无向图 } min_nodes = n; solve(n, graph); cout << "Minimum nodes required: " << min_nodes << endl; return 0; } ``` --- ### 优化策略 - **剪枝**:当当前状态所用节点数已经超过已知最优解时,跳过后续计算。 - **提前终止**:一旦发现某个状态覆盖了全部节点,并且节点数达到理论下限,即可提前结束程序。 - **空间优化**:可以仅保存当前轮次的状态,减少内存占用。 --- ### 总结 本题通过状态压缩动态规划的方法,将原本指数级复杂度的问题压缩到可接受范围内。结合位运算技巧和预处理机制,能够高效地完成状态转移和覆盖判断操作。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值