T1. 三重利刃
注意到,题目中的条件实际上等价于
- 序列中的所有元素都不是完全平方数
- 序列中的任意两个元素的乘积都是完全平方数
条件 \(1\) 使我们可以直接排除 \({a_i}\) 中所有的完全平方数。接下来分析条件 \(2\):
如果两个数的乘积是完全平方数,这意味着乘积中每个质因子的指数均为偶数。那么对于每个质因子,两个数包含这个质因子的次数应当同奇偶。
事实上判定这个相当简单:对于每个数,我们只需要考虑次数为奇数的这些质因子。如果这两个数的这类质因子组成的集合相等,那么这两个数的乘积就会是完全平方数。
综上所述,我们只需要对每个数分解质因子。如果是完全平方数,那么跳过;否则我们把所有次数为奇数的质因子乘起来,然后记录这个乘积。答案就是出现最频繁的乘积的出现次数。
时间复杂度为 \(O(n\log m)\)
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
// linear sieve
vector<bool> isp;
vector<int> ps, pf;
void sieve(int mx) {
isp.resize(mx+1);
pf.resize(mx+1);
rep(i, mx+1) pf[i] = i;
for (int i = 2; i <= mx; ++i) {
if (pf[i] == i) isp[i] = true, ps.push_back(i);
rep(j, ps.size()) {
int x = ps[j]*i;
if (x > mx) break;
pf[x] = ps[j];
if (i%ps[j] == 0) break;
}
}
}
void solve() {
int n;
cin >> n;
vector<int> a(n);
rep(i, n) cin >> a[i];
int ans = 0;
map<int, int> mp;
rep(i, n) {
int x = a[i];
int prod = 1;
map<int, int> cnt;
while (x != 1) {
int p = pf[x];
x /= p;
cnt[p]++;
if (cnt[p]&1) prod *= p;
else prod /= p;
}
mp[prod]++;
if (prod != 1) ans = max(ans, mp[prod]);
}
cout << ans << '\n';
}
int main() {
int t;
cin >> t;
sieve(1e7);
while (t--) solve();
return 0;
}
T2. 七天假日
一个括号串不合法,当且仅当满足以下任意一个条件:
- 整个串的左右括号数量不相等。由于初始串是合法的,所以这个条件在本题中一定不会被满足。
- 存在某个前缀,其中右括号数量多于左括号。
要使括号串变得不平衡,只需在任意前缀中额外添加一个右括号即可。
遍历括号串时,我们维护两个计数器:
open
:当前已遇到的左括号的数量closed
:当前已遇到的右括号的数量
对于每个前缀(包括前缀),需要额外补充的右括号的数量 \(x = open - closed + 1\)
最优不平衡化策略:
将后续 \(x\) 个右括号从当前位置移动到当前前缀的右侧。
此时所需相邻交换次数即为一个候选答案。
时间复杂度为 \(O(n)\)
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
void solve() {
string s;
cin >> s;
int n = s.size();
vector<int> sum(1);
rep(i, n) if (s[i] == ')') sum.push_back(sum.back()+i);
int ans = 1e9;
int open = 0, closed = 0;
rep(i, n) {
int x = open-closed+1;
if (closed+x > n/2) break;
// no. of swaps required to bring next x ')' to the front of string in the same order
int frontCost = sum[closed+x] - sum[closed] - x*(x-1)/2;
// We need to shift x ')' from the positions {0,..,x-1} to the positions {i,..., i+x-1}
int shiftCost = x*i;
int totalCost = frontCost - shiftCost;
ans = min(ans, totalCost);
if (s[i] == '(') open++; else closed++;
}
cout << ans << '\n';
}
int main() {
int t;
cin >> t;
while (t--) solve();
return 0;
}
T3. 辛酸风味
不失一般性,假设 \(n\) 为奇数
先对数组 \(a\) 做升序排序
设最优情况下,最终的中位数/平均值为 \(x\),所需操作次数为 \(y\)
显然可以通过将所有元素加 \(1\)(共 \(n\) 次操作),使统计量提升至 \(x+1\),此时总操作次数为 \(y+n\)
若目标值 \(x\) 可实现,则 \(x+1, x+2, \cdots\) 等更高目标值同样可实现,且每增加 \(1\) 个单位需额外付出 \(n\) 次操作
基于此性质,我们可通过二分确定最终中位数/平均值的目标值,并计算达成该值所需的最小操作次数
现在唯一的问题就是,给定目标值 \(x\),计算使数组的平均值和中位数同时等于 \(x\) 所需的最小操作次数
设数组 \(a\) 的所有元素之和为 \(\mathrm{sum}\)
我们来求使平均值等于 \(x\) 所需的操作次数。根据平均值定义,
\( \frac{\mathrm{sum} + \mathrm{extra}}{n} = x \Rightarrow \mathrm{extra} = nx - \mathrm{sum} \),其中 \(\mathrm{extra}\) 即应执行的操作总次数
我们再求使中位数等于 \(x\) 所需的最小操作数。只需保证下标 \(1 \sim \frac{n-1}{2}\) 的元素都 \(\leqslant x\),下标 \(\frac{n+1}{2} \sim n\) 的元素都 \(\geqslant x\),令该操作次数为 \(y\)。
如果 \(y > \mathrm{extra}\),则无法使平均值等于中位数,因为中位数至少需要 \(y\) 次操作。
如果 \(y <= \mathrm{extra}\),则在将中位数调为 \(x\) 后,可对 \(a_n\) 再执行 \(\mathrm{extra} - y\) 次操作。由于剩余操作只作用于最后一个元素,不会改变中位数,但能将平均值调为 \(x\) 。
时间复杂度为 \(O(n\log n)\)
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
void solve() {
int n;
cin >> n;
vector<ll> a(n);
rep(i, n) cin >> a[i];
sort(a.begin(), a.end());
ll sum = 0;
rep(i, n) sum += a[i];
int m = n/2;
if (n%2 == 0) m--;
ll ac = 1e9, wa = a[m]-1;
while (ac-wa > 1) {
ll wj = (ac+wa)/2;
auto ok = [&]{
ll extra = n*wj - sum;
if (extra < 0) return false;
ll y = 0;
for (int i = m; i < n; ++i) {
y += max(wj-a[i], 0ll);
}
return y <= extra;
}();
(ok ? ac : wa) = wj;
}
ll ans = n*ac - sum;
cout << ans << '\n';
}
int main() {
int t;
cin >> t;
while (t--) solve();
return 0;
}
T4. 行军口袋
设 \(f(i, j)\) 为,以 \(i\) 为根的子树中,红色比黑色多 \(j\) 个时的方案数。显然 \(j \in \{-1, 0, 1\}\),然后根据对称性,可以知道 \(f(i, 1) = f(i, -1)\),所以其实只需考虑 \(j \in \{-1, 0\}\) 。
接下来考虑子树大小:如果以 \(i\) 为根的子树的大小是奇数,那么 \(f(i, 0) = 0\),否则 \(f(i, 1) = 0\) 。
然后考虑如何转移。假设我们现在要计算 \(f(i, 0/1)\),那么首先我们按照子树大小的奇偶性给它的儿子分类,并设 \(u_1, u_2, \cdots, u_x\) 为子树大小为偶数的儿子,\(v_1, v_2, \cdots, v_y\) 为子树大小为奇数的儿子。
可以观察到,偶数大小的子树中,红黑节点数量一定相等,所以不影响 \(i\) 所在子树的红黑节点数量差。所以它们的贡献就是 \(\prod_i f(u_i, 0)\)
然后我们对子树大小为奇数的儿子数量 \(y\) 进行分类讨论:
-
如果 \(y\) 是奇数,那么子树 \(i\) 的节点数量是偶数(因为还有 \(i\))自身,所以 \(f(i, 1) = 0\) 。
设 \(y = 2k+1\),那么只有两种方案:- \(i\) 自身是红色,有 \(k\) 个子树红色更多,\(k+1\) 个子树黑色更多
- \(i\) 自身是黑色,有 \(k\) 个子树黑色更多,\(k+1\) 个子树红色更多
所以奇数大小的子树对答案的贡献为 \(2 \cdot \binom{2k+1}{k} \cdot \prod_i f(v_i, 1)\)
-
如果 \(y\) 是偶数,同理可得 \(f(i, 0) = 0\)。然后设 \(y = 2k\),同样只有两种方案:
- \(i\) 自身是红色,有 \(k\) 个子树是红色,\(k\) 个子树是黑色
- \(i\) 自身是黑色,有 \(k-1\) 个子树是黑色,\(k+1\) 个子树是红色
所以奇数大小的子树对答案的贡献为 \(\left(\binom{2k}{k} + \binom{2k}{k-1}\right) \cdot \prod_i f(v_i, 1)\)