题目链接
CCPC 2024 Jinan Site [F] The Hermit
题目大意
给定一个 \({1, 2, 3 ... m}\) 的集合 \(U\) ,要求从中抽取 \(n\) 个数组成子集 \(S\) ,对于每个 \(S \subset U\),定义 \(gcd(S) \neq min(S)\) 为合法,现在从 \(S\) 中删去若干个数,在保证合法的同时使得 \(||S||\) 最大,记这个最大的集合为 \(T\) ,然后,对 \(||T||\) 求和。
思路
求解思路
暴力枚举全部子集并且求解的复杂度不低于 \(O(C_m^n)\),绝对过不了,答案应当和具体的枚举无关,是一个状态转移的计数问题。朴素的,当 \(S\) 确定时,每次删除 \(min(S)\) ,可以保证得到最大的\(||S||\) , 当 \(m\) 和 \(n\) 很大时,非常难求解,以至于对于第二组样例11 4,答案都达到了几千的量级,无法手动模拟。但是我们可以发现,任一问题的答案,都由 \(m\) 和 \(n\) 完全确定,如果我们有办法把一个很难求解的“大”问题转换为若干个容易求解的子问题就好了,基于这个思路,我想到了记忆化搜索。
和DP一样,记忆化搜索的核心也在于状态的定义与转化,本题已经用 \(m\) 和 \(n\) 给我们构建好了状态,我们记 \(m\) \(n\) 时的答案为 \(a_{m,n}\) ,我们希望 \(a_{m,n}\) 能够由若干个 \(a_{m',n'}\) 转化而来,其中 \(m' \leq m , n' \leq n\),不同时取等,当我们要求解 \(a_{m,n}\) 我们可以入手讨论:
- \(gcd(S) = k , k \notin S\) ,这种情况,显然合法,记合法情况的数量为 \(cnt\),对答案的贡献为 \(cnt \times n\)。其中,\(cnt\) 正向求难求,正难则反,先求出所有 \(k \notin S\) 的子集数量 \(C_{m-1}^n\),再减去\(gcd(S) \neq k\) 的情况,后者可以通过构造方法求解,即枚举 \(gcd(S)\) ,剩余的数都是 \(gcd(S)\) 的 \(k\) 倍。
特别的,求和项中组合数待选取的数的数量为 \(\frac{m}{i}-1\) 需要的数的数量为 \(n-1\),因为你必须要选中 \(gcd(S)\) ,试考虑
4 6 8 10这一组数据,它不应因\(gcd(S) = 2\) 被删去。
- \(gcd(S') = 1, S = S' \cup \{1\}\) ,这种情况,不合法,但是可以通过简单删去1使之合法,对答案的贡献为 \(cnt \times (n-1)\),其余与上述同理,记得必须额外选中\(1\)即可。
- \(gcd(S) = k, k \in S\), 这种情况,通过整个集合除以 \(k\) 转化为第二种情况,具体地,此处我们可以考虑两个集合。
如果我们新定义集合的数乘,你会发现\(B = kA\),其中\(A\)一定满足以上1、2两种情况之一,其中\(1 \leq ka_i \leq m\) ,即 \(\lfloor \frac{1}{k} \rfloor \leq a_i \leq \lfloor \frac{m}{k} \rfloor\),\(B\) 的贡献实际上就是子问题 \(a_{\frac{m}{k},n-1}\) 的答案,其中, \(n-1\) 是因为 \(a_1 = 1\)。
优化思路
预处理阶乘、逆元、逆元的阶乘。
朴素的递归求解,最多不超过 \(log m\) 层,每层枚举 \(k \in [1,m]\),时间复杂度上界大约在 \(O(m^{log m})\) 量级,进入下一层时 \(m\) 按倍数缩小,远低于上界,但是仍太慢,可以朴素记忆化搜索优化。
此外,可以数论分块,时间复杂度上界降至\(O({\sqrt{m}}^{log m})\)
代码
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 100005;
const int MODER = 998244353;
long long fact[N];
long long inv[N];
long long finv[N];long long C(long long a, long long b) {if(b>a) {return 0;}if(b==0) {return 1;}return fact[a]*finv[a-b]%MODER*finv[b]%MODER;
}long long cat(int A, int B) {long long ans = 0;ans ^= A;ans <<= 32;ans ^= B;return ans;
}unordered_map<long long , long long> mem;long long dfs(int m, int n) {if(mem.count(cat(m, n))) {return mem[cat(m, n)];} else if(n <= 1 || m<n) {return 0;} else {long long ans = n*C(m-1, n) + (n-1)*C(m-1, n-1);for(int l=2,r;l<=m;l=r+1) {r = m/(m/l);ans -= (r-l+1)*C(m/l-1, n-1)*n;ans -= (r-l+1)*C(m/l-1, n-2)*(n-1);ans = (ans%MODER+MODER)%MODER;}for(int l = 2, r;l<=m;l=r+1) {r = m/(m/l);ans += dfs(m/l, n-1)*(r-l+1);}ans %= MODER;mem[cat(m, n)] = ans;return ans;}
}int main() {int n,m;cin>>m>>n;fact[0] = fact[1] = 1;finv[0] = finv[1] = 1;inv[0] = inv[1] = 1;for(int i=2;i<=m;i++) {fact[i] = fact[i-1]*i%MODER;inv[i] = 1ll*(MODER - MODER/i)*inv[MODER%i]%MODER;finv[i] = finv[i-1]*inv[i]%MODER;}long long ans = dfs(m, n);cout<<ans<<endl;
}