2025杭电多校1

2025杭电多校1解题报告

update1:补充1001博弈(才学Anti_Nim)2025-7-24


1007 树上LCM

题意

给一颗n个结点组成的树,一个数x,每个结点有一个值,问有多少条路径满足该路径的LCM = x, 路径的LCM即路径上所有结点的LCM。
1 <= n <= 3e5, 2 <= x <= 1e7, a i a_i ai <= 1e9 。

观察

LCM的性质: l c m ( x 1 , x 2 , x 3 . . . , x n ) lcm(x_1,x_2,x_3...,x_n) lcm(x1,x2,x3...,xn) = p 1 k 1 ∗ p 2 k 2 ∗ p 3 k 3 ∗ . . . ∗ p m k m p_1^{k_1}*p_2^{k_2}*p_3^{k_3}*...*p_m^{k_m} p1k1p2k2p3k3...pmkm
其中: p i p_i pi为质因子, k i k_i ki为对应的指数最大。
所以路径上的点至少需要满足三个条件。

  1. a i a_i ai对应的价值能整除x。
  2. a i a_i ai进行质因数分解后的指数需要小于等于 k i k_i ki
  3. 对任意的i,至少存在一个指数等于 k i k_i ki
    x最大能达到1e7,其质因子个数cnt不超过7个。这个数很小,考虑在这里切入。

思路

那些不能整除x的点,肯定无法形成路径。我们可以拆开这些点,这样会形成一些森林。
设目标lcm x的质因子个数为sz,我们对每个点进行状态压缩: 1 << sz, 其中第i位为1,表示这个点第i个质因子的指数小于 k i k_i ki,其中 k i k_i ki为x分解质因数后对应的指数,为0则表示相等。设 F [ i ] F[i] F[i]表示状态为i的路径数, S [ i ] S[i] S[i]表示 i ⊂ 点状态 i\subset点状态 i点状态的点数,易知状态为i的路径个数就是所有 点状态 ⊃ i 点状态\supset i 点状态i的点组成的路径数,对于树上的路径计数,我们可以自然的想到使用并查集维护联通块,联通块上的路径数显然可以利用点的数量 O ( 1 ) O(1) O(1)求出。
现在我们得到了每个 F [ i ] , 0 < = i < 2 s z F[i],0<=i<2^{sz} F[i],0<=i<2sz。现在可以利用容斥原理求出答案。由容斥原理
a n s = ∑ i = 1 ( 2 s z ) − 1 ( − 1 ) popcount ( i ) ⋅ F [ i ] ans = \sum_{i=1}^{(2^{sz}) - 1} (-1)^{\text{popcount}(i)} \cdot F[i] ans=i=1(2sz)1(1)popcount(i)F[i]

代码

// 树上dp,sosdp
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int inf = 1e18;
constexpr int mod = 998244353;
constexpr int N = 2e5 + 10;

void solve() {
    int n, x;
    cin >> n >> x;
    vector<vector<int>> adj(n);
    for(int i = 1; i < n; ++i) {
        int u, v; cin >> u >> v;
        u--, v--;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    vector<int> a(n);
    for(int &x : a) cin >> x;
    vector<int> p, k;
    vector<int> bad(n, 0);
    bool ok = false;
    for(int i = 0; i < n; ++i) {
        if(x % a[i]) {
            bad[i] = 1;
        } else ok = true;
    }
    if(!ok) {
        cout << "0\n";
        return;
    }
    for(int j = 2; j * j <= x; ++j) {
        if(x % j == 0) {
            int cnt = 0;
            while(x % j == 0) {
                ++cnt; x /= j;
            }
            p.push_back(j), k.push_back(cnt);
        }
    }
    vector<int> vis(n, 0);

    if(x > 1) p.push_back(x), k.push_back(1);
    const int sz = (int)p.size();
    const int full = 1LL << sz;
    vector<array<int, 256>> dp(n);
    int ans = 0;
    auto dfs = [&] (auto && self, int u, int fa) -> void {
        vis[u] = 1;
        int now = 0;
        int temp = a[u];
        for(int i = 0; i < sz; ++i) {
            int cnt = 0;
            while(temp % p[i] == 0) temp /= p[i], ++cnt;
            now |= ((cnt == k[i]) * (1LL << i));
        }
        if(now == (full - 1)) ans++;
        dp[u].fill(0);
        dp[u][now] = 1;

        for(int &v : adj[u]) {
            if(v == fa || bad[v]) continue;
            self(self, v, u);

            array<int, 256> sup = dp[u];
            for(int i = 0; i < sz; ++i) {
                for(int j = 0; j < full; ++j) {
                    if(!(j & (1 << i))) sup[j] += sup[j | (1 << i)];
                } 
            }
            for(int i = 0; i < full; ++i) {
                int need = (full - 1) ^ i;
                ans += sup[need] * dp[v][i];
            }
            for(int i = 0; i < full; ++i) {
                dp[u][i | now] += dp[v][i];
            }
        }
    };
    for(int i = 0; i < n; ++i) {
        if(!bad[i] && !vis[i]) dfs(dfs, i, -1);
    }
    cout << ans << '\n';
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t = 1;
    cin >> t;
    while(t--) {
        solve();
    }
    return 0;
}

提供一种莫比乌斯反演的方法

g ( x ) : l c m = x g(x):lcm=x g(x):lcm=x的路径数, f ( x ) : l c m ∣ x f(x):lcm|x f(x):lcmx的路径数。显然有
f ( x ) = ∑ d ∣ x g ( d ) f(x)=\sum_{d|x}g(d) f(x)=dxg(d)
通过莫比乌斯反演有: g ( x ) = ∑ d ∣ x µ ( d ) ∗ f ( x d ) g(x)=\sum_{d|x}µ(d)*f(\frac{x}{d}) g(x)=dxµ(d)f(dx)
显然f(x)的数量可以通过dfs O ( n ) O(n) O(n)求出,发现1e7内的因子个数k不超过200,我们可以预处理出1e7内的莫比乌斯函数,对于每个因子d, O ( n ) O(n) O(n)计算 f ( d ) f(d) f(d),复杂度为 O ( k n + N ) O(kn+N) O(kn+N)可以接受。

代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int inf = 1e18;
constexpr int mod = 998244353;
constexpr int N = 1e7;
// 设g(x): lcm = x的路径,f(x): lcm整除x的路径。
// 显然f(x) = 1 * g(x)
// 则 g(x) = µ * f(x)
// 线性筛预处理莫比乌斯函数µ即可.
int mu[N + 1], minp[N + 1], p[N + 1];
int cnt = 0;
void init() {
    mu[1] = 1;
    for(int i = 2; i <= N; ++i) {
        if(!minp[i]) p[++cnt] = i, mu[i] = -1;
        for(int j = 1; j <= cnt && i * p[j] <= N; ++j) {
            minp[i * p[j]] = p[j];
            if(i % p[j] == 0) {
                mu[i * p[j]] = 0;
                break;
            }   
            mu[i * p[j]] = -mu[i];
        }
    }
}
void solve() {
    int n, x;
    cin >> n >> x;
    vector<vector<int>> adj(n);
    for(int i = 1; i < n; ++i) {
        int u, v; cin >> u >> v;
        u--, v--;
        adj[u].push_back(v), adj[v].push_back(u);
    }
    vector<int> a(n);
    for(int &x : a) cin >> x;
    vector<int> ok(n);
    vector<int> siz(n);
    int res = 0;
    auto dfs = [&](auto &&self, int u, int fa) -> void {
        siz[u] = ok[u];
        for(int v : adj[u]) {
            if(v != fa) self(self, v, u), siz[u] += siz[v];
        }
        if(ok[u] && (fa == -1 || !ok[fa])) res += siz[u] * (siz[u] + 1) / 2;
        if(!ok[u]) siz[u] = 0;
    };
    auto cal = [&](int y) -> int {
        for(int i = 0; i < n; ++i) {
            ok[i] = (y % a[i] == 0);
        }
        res = 0;    
        dfs(dfs, 0, -1);
        return res;
    };
    int ans = 0;
    for(int i = 1; i <= x; ++i) {
        if(x % i == 0 && mu[i]) {
            ans += mu[i] * cal(x / i);
        }
    }
    
    cout << ans << '\n';
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    init();
    int t = 1;
    cin >> t;
    while(t--) {
        solve();
    }
    return 0;
}

1001 博弈

前置知识:Nim和anti_Nim
我们记 T = ⨁ i = 1 n x i T=\bigoplus_{i=1}^{n}x_i T=i=1nxi
对于Nim游戏有:若 T = 0 T=0 T=0则必败,否则必胜。
对于anti_Nim游戏分两种情况:
如果所有堆全部为1,则奇败偶胜。
至少有一堆大于1,则 T = 0 T=0 T=0必败,否则必胜。

题意

n n n个房间,每个房间有 k k k堆石头,具体的每一堆有 k i k_i ki堆石头, A l i c e , B o b Alice,Bob Alice,Bob可以轮流在任意房间取第任意堆的任意个石头,最后无法取石头的输掉游戏。规定必须在一个房间取完才可以去另外一个房间取, A l i c e , B o b Alice,Bob Alice,Bob都以最优状态取,但是 A l i c e Alice Alice 可以规定房间顺序,问有多少种房间的排列让 A l i c e Alice Alice必胜,答案对1e9 + 7取模。

Hint1:先弱化问题,考虑所有房间的所有堆都只有1个石头,这个问题比较显然。我们定义奇数房间为有奇数个堆的房间,反之亦然。当有奇数个奇数房间时,必胜,反之必败。
Hint2:偶数房间显然对答案没有影响,因为这不会反转先后手。
Hint3:现在考虑存在房间里面至少有一个多于一个石头的堆。这个是否会对先后手反转有一定影响?

题解

全为1的情况已经判完。现在考虑不全为1的同时不考虑偶数房间,因为它对答案没有影响我们可以随便放。有两种可能一个是 T = 0 T=0 T=0的房间,我们定义这种房间为坏房间,反之为好房间。如果我们碰到的第一个不全为1的房间为好房间,我们发现无论是Nim还是anti_nim,它都是必胜的,即先手可以决定后面的出手顺序,这就是必胜态,故它前面的奇数房间不能反转出手顺序,即前面只能有偶数个奇数房间。反之,如果我们碰到的第一个不全为1的房间为坏房间,我们发现无论是Nim还是anti_nim,它都是必败的,即后手可以决定后面的出手顺序,这就是必败态,故它前面的奇数房间必须要反转出手顺序,即前面得有奇数个奇数房间来进行反转。
我们考虑枚举前面有多少个奇数房间统计答案。

代码

#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int inf = 1e18;
constexpr int mod = 1e9 + 7;
constexpr int N = 1e6 + 10;
int qpow(int base, int index) {
    int res = 1;
    while(index) {
        if(index & 1) res = res * base % mod;
        base = base * base % mod;
        index >>= 1;
    }
    return res;
}
int f[N];
void fac() {
    f[0] = 1;
    for(int i = 1; i < N; ++i) {
        f[i] = i * f[i - 1] % mod;
    }
}
int comb(int x, int y) {
    return f[x] * qpow(f[y], mod - 2) % mod * qpow(f[x - y], mod - 2) % mod;
}
void solve() {
    int n;
    cin >> n;
    array<int, 4> cnt = {};
    for(int i = 0; i < n; ++i) {
        int k; cin >> k;
        bool ok = true;
        int s = 0;
        for(int j = 0; j < k; ++j) {
            int x; cin >> x;
            s ^= x;
            if(x > 1) ok = false;
        }
        if(ok) {
            if(s) cnt[0]++;
            else cnt[1]++;
        } else {
            if(s) cnt[2]++;
            else cnt[3]++;
        }
    }
    if(cnt[0] + cnt[1] == n) {
        if(cnt[0] & 1) {
            cout << f[n] << '\n';
        } else cout << "0\n";
        return ;
    }
    // 放多少个奇数在前面
    int ans = 0;
    for(int i = 0; i <= cnt[0]; ++i) {
        int temp = 1;
        if(i & 1) {
            temp = f[i] * comb(cnt[0], i) % mod * cnt[3] % mod;
            temp = temp * f[cnt[2] + cnt[3] - 1 + cnt[0] - i] % mod;
        } else {
            temp = f[i] * comb(cnt[0], i) % mod * cnt[2] % mod;
            temp = temp * f[cnt[2] + cnt[3] - 1 + cnt[0] - i] % mod;
        }
        ans = (ans + temp) % mod;
    }
    // 偶数随便放。
    for(int i = 1; i <= cnt[1]; ++i) {
        ans = (ans * (n - cnt[1] + i)) % mod;
    }
    cout << ans << '\n';
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    fac();
    int t = 1;
    cin >> t;
    while(t--) {
        solve();
    }
    return 0;
}
### 杭电比赛与童年经历 杭电比赛作为国内知名的 ACM/ICPC 训练平台之一,吸引了大量热爱算法的学生参与其中。对于许参赛者来说,这些比赛不仅是技术上的挑战,更是一段难忘的成长历程。 从早期的竞赛启蒙来看,很选手最初接触编程和算法完全是因为兴趣使然[^1]。例如,在高中阶段参加 OI 或类似的训练活动时,许人并未意识到其重要性,仅仅将其视为一种娱乐方式。这种轻松的心态反而可能带来意想不到的好成绩,比如某次模拟赛中因团队合作默契而意外夺冠的经历。 当提到杭电比赛的具体题目或经历时,这类赛事通常会设计一系列覆盖广泛知识点的问题来考验参赛者的综合能力。以下是几个典型的例子: #### 题目示例 1. **基础数据结构应用** 这类题目往往考察数组、链表等基本概念的应用场景。 ```cpp // 示例代码:简单的数组遍历求最大值 int findMax(int arr[], int n) { int maxVal = arr[0]; for (int i = 1; i < n; ++i) { if (arr[i] > maxVal) { maxVal = arr[i]; } } return maxVal; } ``` 2. **动态规划入门** 动态规划是 ACM 中非常重要的技巧之一,初学者可以通过经典的背包问题熟悉该方法。 ```python # 背包问题实现 def knapsack(W, wt, val, n): dp = [[0 for _ in range(W + 1)] for __ in range(n + 1)] for i in range(1, n + 1): for w in range(1, W + 1): if wt[i-1] <= w: dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i-1]] + val[i-1]) else: dp[i][w] = dp[i-1][w] return dp[n][W] ``` 3. **图论初步探索** 图论相关题目常涉及最短路径计算或者连通性分析等内容。 ```java import java.util.*; public class ShortestPath { static final int INF = Integer.MAX_VALUE / 2; public static void dijkstra(List<List<int[]>> adjList, int start, int[] dist){ PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1])); Arrays.fill(dist, INF); dist[start] = 0; pq.add(new int[]{start, 0}); while(!pq.isEmpty()){ int[] current = pq.poll(); int u = current[0]; if(current[1] > dist[u]) continue; for(int[] edge : adjList.get(u)){ int v = edge[0], weight = edge[1]; if(dist[v] > dist[u] + weight){ dist[v] = dist[u] + weight; pq.add(new int[]{v, dist[v]}); } } } } } ``` 通过上述实例可以看出,无论是童年的简单尝试还是后来深入学习后的复杂解法,每一次练习都为未来打下了坚实的基础。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值