种树
首先看到乘积、因数这类的,就很难不想到做质因数分解。对于一个正整数 ,如果它的唯一质因数分解为 ( 是质因子 在 中的出现次数),那么它的因数个数 。直观来看就是每个质因数都有 种取指数的方式。
现在考虑 分过来对 的影响。分质因子考虑,枚举质因子,假设其在 中的出现次数为 ,在 中的出现次数为 ,那么相当于你可以对任意 进行 次 操作(分一个质因子过去)。
显然,要最大化 ,每次你就把最小的 即可。最终这个质因子对答案的贡献就是 。把所有质因子的贡献都乘起来即可。时间复杂度 。
const LL kMaxN = 1e4 + 5, kMod = 998244353;
LL a[kMaxN], n, w, ans = 1;
priority_queue<LL, vector<LL>, greater<>> q;
void solve(LL x, LL k) {
for (LL i = 1; i <= n; i++) {
LL cnt = 0;
for (; a[i] % x == 0; a[i] /= x) {
cnt++;
}
q.push(cnt + 1);
}
while (k--) {
LL x = q.top();
q.pop();
q.push(x + 1);
}
for (; q.size(); q.pop()) {
ans = ans * q.top() % kMod;
}
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> w;
for (LL i = 1; i <= n; i++) {
cin >> a[i];
}
for (LL i = 2; i * i <= w; i++) {
if (w % i) {
continue;
}
LL cnt = 0;
for (; w % i == 0; w /= i) {
cnt++;
}
solve(i, cnt);
}
if (w > 1) {
solve(w, 1);
}
for (LL i = 1; i <= n; i++) {
for (LL j = 2; j * j <= a[i]; j++) {
if (a[i] % j) {
continue;
}
LL cnt = 0;
for (; a[i] % j == 0; a[i] /= j) {
cnt++;
}
ans = ans * (cnt + 1) % kMod;
}
if (a[i] > 1) {
ans = ans * 2 % kMod;
}
}
cout << ans << '\n';
return 0;
}汪了个汪
首先 的条件完全属于 ,所以直接走 所组成的无序二元组数量为 ,正好对应上了输出的 所组成的 对相邻二元组。所以所有可能的二元组数量都必须用上。
容易将问题转成图论模型:在 个点所组成的双向完全图上,找 条简单路径,第 条路径长 ,每条路径分别从 出发,同时互不相等地覆盖了所有的边。
这个问题非常的复杂,我想了两年半才有些许思路。
不难发现在所有二元组 当中,以 的大小来分类, 的有 对, 的有 对,这正好对应上了跨第 列和第 列的二元组数量是 个。
所以我们尝试将 的二元组作用在第 列和第 列当中。具体来说,设某一行第一个数为 ,那么这一行的数分别是
那么我们可以枚举 作为每一行的第一个元素。 的话当然是一正一负,这样可以最大化长度。当值超过 的时候,就不用往后继续生成了。
实操下来,对于 ,生成出来的序列长度正好是 的,于是我们按照长度从小到大的顺序输出即可。
为什么这么生成一定不会产出相同的相邻二元组?
因为第一个数 是不一样的。首先行内肯定不会出现相同二元组,因为所有的 都不同。对于某行的第 个元素,它所形成的二元组和它正上/下方的 相同。它相对于 的增量都是 的,但是由于第一个数 不相同,所以和上下的二元组肯定是不同的。所以我们不会生成出相同的相邻二元组。
时间复杂度 。
const int kMaxN = 4005;
int id[kMaxN], n, t;
vector<vector<int>> ans;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> t;
for (int i = 1; i <= n; i++) {
ans.emplace_back();
for (int j = 1, x = i; j <= n; j++) {
ans.back().push_back(x);
if (j % 2) {
x += j;
} else {
x -= j;
}
if (x < 1 || x > n) {
break;
}
}
}
for (int i = 0; i < ans.size(); i++) {
id[ans[i].size()] = i;
}
for (int i = 1; i <= n; i++) {
for (int j : ans[id[i]]) {
cout << j << ' ';
}
cout << '\n';
}
return 0;
}挑战 NPC IV
我使用的是暴力。暴力计算 个全排列所对应的答案,然后直接 nth_element 求出第 大。时间复杂度 ,成功获得 分。
然后是赛后改题环节。
首先对于 ,排序后第 个数会在 个区间当中出现过,所以把 小的放中间, 大的放两边即可。
对于 一样的,彼此之间怎么排都不会改变答案。所以当 时,最小的答案会出现超过 ,此时相当于 。
计算方式是令 为 的数量,求 。事实上你求出来 时应该是大约 ,这是因为有些不能完全居中,会偏左或偏右,所以实际上这种情况已经超过 了。
所以对于 ,我们直接考虑 特化的 做法。直接枚举 ,优先小的的放中间,通过数学计算算出 前面的系数并累加。
具体计算方式
计算 的系数,即 。
首先把 拆成 减去 ,应用乘法分配律,得到
前者应用等差数列求和公式,后者用
做差即可。得到
对于 ,也是因为最终的答案不超过大约 这个数量级,所以直接设计状态 表示 的分别有 个,且总优美度为 的合法排列数量。那么我们只需要从小到大枚举 ,计算 对应的数量,就可以找到第 小的 了。用记忆化搜索优化常数,规避无用状态。时间复杂度 ,其中 表示答案值域,大约 。
四暗刻单骑
我采用的是暴力。令 表示当前手可以赢, 表示不能赢但是可以平局, 表示必输。那么爆搜状态 就表示当前摸第 个牌,当前手手牌为 ,对面为 。摸完 后当前手有两种操作:
- 不要摸的 ,直接打出去。
- ,那么打出去就直接输了,这种情况值为 。
- 否则递归计算 ,然后取反(这就是我的值的特殊性,可以直接取相反数交换状态)。
- 摸了 攥手里,把 打出去。
- ,打出去直接输,值为 。
- 否则递归计算 , 然后取反。
需要注意搞之前先把 判了(平局),然后把 判了(必胜)。
不难发现对于相同的 , 所对应答案是一定的,所以可以做记忆化搜索。时间复杂度 。成功获得 分。