题目中给的东西其实就是要求排列对应的坐标点落在大圆和小圆之间求排列的数量。记 表示满足 的 的数量, 表示满足 的 的数量。则排列 的限制条件就可以看作是 。
考虑对内圆 的限制做容斥,钦定排列前 个位置中,有 个位置是不满足 ,则记这样的排列的数量为 则直接容斥可得答案为 。
考虑算 的值。先给一个暴力的做法,设 表示 位置的上限,则 要么为 要么为 。考虑已知 时合法的排列 数量怎么算。有一个结论是,如果把 数组内所有值从小到大排序,则满足限制的排列数为 。证明考虑按照上界从小王大分配数字时,处理第 个位置可以使用 这 个数字,前面也已经使用了 个数字,而这 个数字一定在上界范围内,因此共有 种剩余的可用选择。因此直接枚举 的值,其中恰好有 个上界选的是 然后对排列数求和就得到了一个暴力的做法。
考虑进一步优化时间复杂度。容易想到 dp,套路的把所有的 和所有满足 的 按照值从小到大放到一起,然后按值从小到大做扫描线,设 表示当前已经选择了 个 时的贡献,则如果当前的是一个 而在这之前已经出现了 个必须选择的 (这里说 必须选择是指 的 此时 的限制不存在)那么当前 前面留下的数共有 个,因此 在最终排序的下标就是 ,其贡献就是 ,因此转移就是 。
然后考虑如果当前遇到了一个 ,则需要进一步分类讨论:
如果选 则她前面已经有 个被选中的数,所以其的贡献就是 ,因此转移就形如 。
如果不选 则之后就必须选对应的 ,考虑贡献提前处理,对应 的位置已经是确定的了,因此固定最终选 个 加上 个必须选的 ,这些大 前面一定有 个数,而当前之前已经经过了 个 ,其中只有 个 被选,所以共有 个 被放弃了,她们对应的 都会排在当前这个 的前面,因此当前这个 的最终下标就是 ,其贡献就是 ,转移就形如 。最后更新 即可。
扫描完之后 就是恰好选 个 的总共先,也就是 。代入上面的容斥式子直接算答案即可,总时间复杂度为 。
:::success[Code]
namespace lowspeed_song {
inline void init() {
}
int F[N], nF[N];
inline void sol([[maybe_unused]]int __testcase_id) {
int n, mod; cin >> n >> mod;
vector<pair<int, int>> a;
for (int i = 0; i < n; ++i) {
int l = 0, r = 2 * n - 1;
while ((l + 1) * (l + 1) + i * i < n * n) ++l;
while (r * r + i * i > 4 * n * n) --r;
a.emplace_back(l, r);
}
for (int i = n; i < 2 * n; ++i) {
int r = 2 * n - 1;
while (r * r + i * i > 4 * n * n) --r;
a.emplace_back(r, 0);
}
sort(a.begin(), a.end());
function<int(int)> calc = [&](int k) {
fill(F, F + n + 1, 0), F[0] = 1;
int c1 = 0, c2 = 0;
for (auto &[l, r] : a) {
fill(nF, nF + n + 1, 0);
if (!r) {
for (int j = 0; j <= c2; ++j) nF[j] = F[j] * (l - c1 - j + 1) % mod;
++c1;
} else {
for (int j = 0; j <= c2; ++j) nF[j + 1] = (nF[j + 1] + F[j] * (l - c1 - j + 1) % mod) % mod, nF[j] = (nF[j] + F[j] * (r - (n + c2 - j + k) + 1) % mod) % mod;
++c2;
} copy(nF, nF + n + 1, F);
} return F[k];
};
int res = 0;
for (int k = 0; k <= n; ++k)
if (k & 1) res = (res - calc(k) + mod) % mod;
else res = (res + calc(k)) % mod;
cout << res << '\n';
}
} // namespace lowspeed_song:::
喜欢的话,留下你的评论吧~