T1. 三重利刃

注意到,题目中的条件实际上等价于

  1. 序列中的所有元素都不是完全平方数
  2. 序列中的任意两个元素的乘积都是完全平方数

条件 \(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)\)