T1. 数学作业

发现 \(a-b\) 太大会很快超过题目所限的范围,所以 \(a-b\) 值并不大。

然后枚举差值 \(d\),发现 \(\frac{(x+d)!}{x!}\) 关于 \(x\) 单调递增。所以可以二分判断存不存在 \(x\) 满足 \(\frac{(x+d)!}{x!} = n\)

注意特判 \(n=1\)

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;
using l3 = __int128;

void solve() {
    ll n;
    cin >> n;
    
    if (n == 1) {
        puts("inf");
        return;
    }
    
    int ans = 0;
    l3 prod = 1;
    for (int d = 1;; ++d) {
        prod *= d+1;
        if (prod > n) break;
        
        ll ac = 0, wa = n;
        while (abs(ac-wa) > 1) {
            ll wj = (ac+wa)/2;
            
            bool ok = true;
            l3 fact = 1;
            for (int i = 1; i <= d; ++i) {
                fact *= wj+i;
                if (fact > n) {
                    ok = false;
                    break;
                }
            }
            
            if (fact == n) {
                ans++;
                break;
            }
            if (ok) ac = wj; else wa = wj;
        }
    }
    
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}

T2. 回文串

对于一个长为 \(M\) 的字符串 \(S\),它是回文当且仅当对于任意的 \(1 \leqslant i \leqslant M\) 都有 \(S_i = S_{M-i+1}\)

特别地,要使这 \(N\) 个字符串都成为回文串,需要满足:

  • \(C_i\) 表示由所有字符串的第 \(i\) 个字符组成的字符串,也就是 \(C_i = S_{1,i}S_{2,i} \ldots S_{N,i}\)\(C_i\) 本质上相当于这些字符串的第 \(i\) 列。
  • 那么对于每个 \(1 \leqslant i \leqslant M\),都必须有 \(C_i = C_{M+1-i}\)

注意到给定的操作只允许我们在同一列内移动字符 —— 无法将其移动到其他列。

所以,我们只需计算以下内容:

  • 对于每个 \(1 \leqslant i \leqslant M\),在允许对两串都进行相邻交换的情况下,求出将 \(C_i\) 变为 \(C_{M+1-i}\) 的最小相邻交换次数。
  • 然后,把所有列对的答案加起来
  • 如果有任何列对无法相等,则答案为 \(-1\)

因此,我们只剩下一个子问题:给定两个字符串 \(S\)\(T\):给定两个字符串 \(S\)\(T\),在允许对两串都进行相邻交换的情况下,求使 \(S=T\) 所需的最少操作次数。

这可以通过一个有趣的观察来解决:只在一个字符串上执行交换,而不改变另一个字符串就足够了!

证明:
假设我们对 \(S\)\(T\) 都进行了相邻交换,并且最终它们相等。令 \(T\) 上的最后一次交换为 \((i, i+1)\)
然后,我们可以不在 \(T\) 上执行这次交换,而是在 \(S\) 上将其作为最后一次操作。这样依然保持 \(S=T\),同时在 \(T\) 上少执行一次操作。
反复进行这个过程将使我们达到 \(S=T\) 这个状态,但没有按要求对 \(T\) 进行任何操作。

因此,我们保持 \(T\) 不变,计算将 \(S\) 变为 \(T\) 所需的最小相邻交换次数。
这是一个相当经典的问题。

基本思路如下:

  • 对每个位置 \(i\),计算 \(S_i\) 应该去到的位置,记为 \(pos_i\)
  • 可以按以下方式来做:
    • 逐字符处理
    • \(S\) 中第 \(1\) 次出现的 a 应去到 \(T\) 中第 \(1\) 次出现的 a 的位置,\(S\) 中第 \(2\) 次出现的 a 应去到 \(T\) 中第 \(2\) 次出现的 a 的位置,以此类推。
    • 对每个字符都这样处理。
    • 只需知道 \(S\)\(T\) 中各字符的位置,即可直接建立映射关系。
  • 一旦我们知道每个字符最终的位置,问题本质上要求达到此配置的最小交换次数。
  • 这实际上就是计算数组 \(pos\) 的逆序数,可通过多种方法在 \(O(N\log N)\) 时间复杂度内高效求解。
代码实现
#include <bits/extc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using namespace __gnu_pbds;
using ll = long long;
template<class T>
using Tree = tree<T, null_type, less<T>, rb_tree_tag, tree_order_statistics_node_update>;

ll f(vector<int>& a) {
    ll res = 0;
    Tree<int> t;
    for (int x : a) {
        res += t.size() - t.order_of_key(x);
        t.insert(x);
    }
    return res;
}

ll calc(string s, string t) {
    int n = s.size();
    vector<vector<int>> pos(26);
    rep(i, n) pos[s[i]-'a'].push_back(i);
    
    vector<int> ptr(26), reach(n);
    rep(i, n) {
        int c = t[i]-'a';
        if (ptr[c] == pos[c].size()) return -1;
        reach[i] = pos[c][ptr[c]];
        ++ptr[c];
    }
    return f(reach);
}

void solve() {
    int n, m;
    cin >> n >> m;
    
    vector<string> s(n);
    rep(i, n) cin >> s[i];
    
    ll ans = 0;
    rep(j, m-1-j) {
        string a, b;
        rep(i, n) {
            a += s[i][j];
            b += s[i][m-1-j];
        }
        ll now = calc(a, b);
        if (now == -1) {
            ans = -1;
            break;
        }
        ans += now;
    }
    
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}

T3. 记忆恢复

CF1526E

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

const int mod = 998244353;
//const int mod = 1000000007;
struct mint {
    ll x;
    mint(ll x=0):x((x%mod+mod)%mod) {}
    mint operator-() const {
        return mint(-x);
    }
    mint& operator+=(const mint a) {
        if ((x += a.x) >= mod) x -= mod;
        return *this;
    }
    mint& operator-=(const mint a) {
        if ((x += mod-a.x) >= mod) x -= mod;
        return *this;
    }
    mint& operator*=(const mint a) {
        (x *= a.x) %= mod;
        return *this;
    }
    mint operator+(const mint a) const {
        return mint(*this) += a;
    }
    mint operator-(const mint a) const {
        return mint(*this) -= a;
    }
    mint operator*(const mint a) const {
        return mint(*this) *= a;
    }
    mint pow(ll t) const {
        if (!t) return 1;
        mint a = pow(t>>1);
        a *= a;
        if (t&1) a *= *this;
        return a;
    }

    // for prime mod
    mint inv() const {
        return pow(mod-2);
    }
    mint& operator/=(const mint a) {
        return *this *= a.inv();
    }
    mint operator/(const mint a) const {
        return mint(*this) /= a;
    }
};
istream& operator>>(istream& is, mint& a) {
    return is >> a.x;
}
ostream& operator<<(ostream& os, const mint& a) {
    return os << a.x;
}

struct modinv {
  int n; vector<mint> d;
  modinv(): n(2), d({0,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(-d[mod%n]*(mod/n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} invs;
struct modfact {
  int n; vector<mint> d;
  modfact(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*n), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} facts;
struct modfactinv {
  int n; vector<mint> d;
  modfactinv(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*invs(n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} ifacts;
mint comb(int n, int k) {
  if (n < k || k < 0) return 0;
  return facts(n)*ifacts(k)*ifacts(n-k);
}

int main() {
    int n, k;
    cin >> n >> k;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i], a[i]--;
    
    vector<int> p(n+1);
    p[n] = -1;
    rep(i, n) p[a[i]] = i;
    
    int cnt = 1;
    rep(i, n-1) {
        if (p[a[i]+1] > p[a[i+1]+1]) cnt++;
    }
    k -= cnt;
    
    mint ans = comb(n+k, k);
    cout << ans << '\n';
    
    return 0;
}

T4. 大战

考虑倒着构造这个方案。

可以认为摧毁防御塔实际上是将序列中对应的元素置为 \(0\)

初始时,我们假设每个元素的值均为 \(0\),然后再从后往前给序列填数。为了方便表述,我们设 \(f(i)\) 为序列 \([0, 0, \ldots, a_i, a_{i+1}, \ldots, a_n]\) 的答案,显然有 \(f(n+1)=0\)

然后考虑如何从 \(f(i+1)\) 推出 \(f(i)\)

考虑确定 \(a_i\) 的摧毁时机。我们可以枚举 \(k\),然后假设在 \(p=k\) 时摧毁 \(a_i\) 。此时 \(a_i\) 对答案有以下贡献:

  • \(p<k\) 时,每次选择向右移动 \(1\) 格的时候,\(a_i\) 都会被计入伤害,总贡献为 \(+a_i \cdot (k-1)\)
  • 对于 \(i<j \leqslant k\) 的所有下标 \(j\),如果 \(a_j > a_i\) ,那么 \(a_j\) 会先于 \(a_i\) 被删掉。假设有 \(cnt\) 个这样的下标 \(j\),那么 \(a_i\) 会再被推迟 \(cnt\) 次操作被摧毁,总贡献为 \(+a_i \cdot cnt\)
  • 对于 \(i<j\) 的所有下标 \(j\),如果 \(a_j \leqslant a_i\) ,那么 \(a_j\) 会后于 \(a_i\) 被删掉。假设这些 \(a_j\) 的和为 \(sum\) ,那么由于摧毁 \(a_i\) 需要花费 \(1\) 次操作,所以这些元素会再晚 \(1\) 次操作被摧毁,总贡献为 \(+sum\)

于是有 \(f(i) = f(i+1) + \min_k \{ a_i \cdot (cnt+k-1) + sum\}\)\(cnt\)\(sum\) 可以在枚举 \(k\) 时维护。

总复杂度为 \(O(n^2)\)