A. tb的区间问题

题意

tb 给了 fc 一个长度为 n 的数组 A , fc 对 A 进行 k 次如下操作:

删除数组第一个元素或者删除数组最后一个元素。

求最后得到的数组和的最大值。

大概思路

滑动窗口秒了

参考代码

void solve() {
	int n, k;
    std::cin >> n >> k;
    
    int l = n - k;
    i64 ans = 0;
    std::vector<i64> a(n);
    for (auto &i : a) std::cin >> i;
    
    i64 tmp = 0;
    for (int i = 0; i < n; i++) {
        tmp += a[i];
        if (i - l >= 0) { 
            tmp -= a[i - l];
        }
        ans = std::max(ans, tmp);
    }
    std::cout << ans;
}

B. tb的字符串问题

题意

tb 给了 fc 一个字符串。

fc 对字符串可以进行若干次 (可能是0) 次如下操作:

选择子串 ‘‘fc’’ 或者子串 ‘’tb’’ ,将其从字符串中删去。

求最后剩下字符串的最短长度。

子串:原字符串中下标连续的一段字符串。

大概思路

直接用栈贪心地匹配fc和tb就行

参考代码

void solve() {
	int n;
    std::cin >> n;
    std::string s;
    std::cin >> s;
    
    std::stack<char> stk;
    for (auto &c : s) {
        if (stk.size() && (stk.top() == 'f' && c == 'c' || stk.top() == 't' && c == 'b')) {
            stk.pop();
        } else {
            stk.push(c);
        }
    }
    std::cout << stk.size();
}

C. tb的路径问题

题意

tb 给了 fc 一张有 n×n 个格点的图。

图上每个格点都写着一个数。第 i 行第 j 列的格点上写着的数字为 i 和 j 的最大公约数。

现在 fc 需要从第 1 行第 1 列出发,去往第 n 行第 n 列处的格点,fc 可以消耗一点能量移向相邻的格点。

在任何时候,设 fc 所位于的格点上所写的数字为 x ,如果 x 不为 1 ,他可以使用传送阵传送到任何数字为 x 的格点,此操作不消耗能量。

求 fc 到达第 n 行第 n 列所消耗的最少能量。

相邻:如果用 (x,y) 表示第 x 行第 y 列的位置,(x,y) 与 (x+1,y),(x,y+1),(x−1,y),(x,y−1) 相邻。

大概思路

手玩一下样例,取n为10左右,把除1以外的格子标出来,容易发现gcd为2的格子实在是太多了。

行/列 1 2 3 4 5 6 7 8 9
1 1
2 2 2 2 2
3 3 3 3
4 2 4 2 2
5 5
6 2 3 2 6 2 3
7 7
8 2 2 2 8
9 3 3 9

我们当 $n$ 很大时,我们不妨先花两步走到 $(2,2)$,然后分类讨论:

  • 当 $n$ 为偶数时,$\gcd(n, n - 2) = 2$,我们只需要再花费2步,就能从 $(n - 2,n)$ 转移到 $(n, n)$
  • 当 $n$ 为奇数时,$n - 1$为偶数,我们可以先转移到 $(n-1,n-1)$,再花2步转移到 $(n,n)$

然后注意特判 $n$ 很小的情况($n = 1, 2, 3$)就可以了。

参考代码

void solve() {
    int n;
    std::cin >> n;
     
    if (n == 1) {
        std::cout << 0;
        return;
    } else if (n == 2) {
        std::cout << 2;
        return;
    } else if (n == 3) {
        std::cout << 4;
        return;
    } else {
        if (n % 2 == 0) {
            std::cout << 4;
        } else {
            std::cout << 6;
        }
        return;
    }
}

D. tb的平方问题

题意

tb 给了 fc 一个数组 A 。

随后, tb 对 fc 进行了 q 次询问,每次询问 tb 给 fc 一个 x ,需要 fc 给出包含了 x 位置且区间和为完全平方数的连续子数组个数。

大概思路

注意到n比较小,为1e3,完全可以$O(n^2)$暴力预处理出所有元素和为完全平方数的区间。然后用差分进行快速区间修改,最后前缀和统计即可。

不知道为什么没想到前缀和,直接上的树状数组

参考代码

template<typename T>
struct BIT {
	std::vector<T> a;
	int n;
	BIT(int n_) {//下标从1开始
		a.clear();
		a.resize(n_ + 1);
		n = n_;
	}
	BIT() {}
	BIT(const BIT& bit) {//[可选] 复制bit(很少用)
		n = bit.n;
		a = bit.a;
	}
	T getsum(int x) {//获取[1, x]的和		O(logn)
	    T s = 0;
	    while (x) {
	        s += a[x];
	        x -= (x &- x);
	    }
	    return s;
	}
	void add(int x, T val) {//单点增加x处的值	O(logn)
	    while (x <= n) {
	        a[x] += val;
	        x += (x &- x);
	    }
	}
};
//附:想区间修改,区间查询的话,可以用这个维护差分数组(不过都只支持加减)
//区间推平,区间加乘,等操作,请换线段树


void solve() {
	int n, q;
    std::vector<i64> a(n + 1), pre(n + 1);
    for (int i = 1; i <= n; i++) std::cin >> a[i];
    
    for (int i = 1; i <= n; i++) {
        pre[i] = a[i] + pre[i - 1];
    }
    
    auto ck = [&](i64 x) {
        i64 x_ = std::sqrt(x);
        return ((x_ - 1) * (x_ - 1) == x) || ((x_) * (x_) == x) || ((x_ + 1) * (x_ + 1) == x);
    };
    BIT<i64> bit(n + 1);
    for (int i = 1; i <= n; i++) {
        for (int j = i; j <= n; j <= n) {
            if (ck(pre[j] - pre[i - 1])) {
                bit.add(i, 1);
                bit.add(j + 1, -1);
            }
        }
    }
    while (q--) {
        int x;
        std::cin >> x;
        std::cout << bit.getsum(x) << "\n";
    } 
}

E. tb的数数问题

题意

tb 给了 fc 一个包含若干个数字的可重集合 $A$ ,如果我们说一个数字 $x$ 是好的当且仅当 $\forall d∣x$ ,有 $d∈A$。

现在,fc 想知道有多少个不同的正整数是好的,请你告诉他吧。

$d∣x$ : 表示 $d$ 为 $x$ 的约数。

$∀ d∣x$ ,有 d∈Ad∈A : $x$ 的任何约数都至少在 $A$ 中出现一次。

大概思路

一个很像dp的东西,设 $P_i$ 为 $a_i$ 的所有质因数构成的集合,那么转移方程如下:

$$ dp[a_i] = \underset{p \in P_i}{{\LARGE\mathrm \&}} dp[\frac{a_i}{p}]$$

然后统计dp[i]即可,复杂度应该是$O(n\log\max(a_i))$的。

参考代码

struct Sieve {
    std::vector<int> mpf; // minimum prime factor
    std::vector<int> prime;

    Sieve(int n) : mpf(n) {
        for (int i = 2; i < n; i++) {
            if (mpf[i] == 0) {
                prime.push_back(i);
                mpf[i] = i;
            }
            for (int j = 0; j < prime.size() and i * prime[j] < n; j++) {
                mpf[i * prime[j]] = prime[j];
                if (prime[j] == mpf[i])
                    break;
            }
        }
    }

    // get all factors
    std::vector<int> getDiv(int x) const {
        std::vector<int> _div(1, 1);
        while (x > 1) {
            int d = mpf[x];
            int l = 0, r = _div.size();
            while (x % d == 0) {
                for (int k = l; k < r; k++) {
                    _div.push_back(_div[k] * d);
                }
                x /= d;
                l = r, r = _div.size();
            }
        }
        return _div;
    }
};

Sieve si(1e6 + 1);

void solve() {
	int n;
    std::cin >> n;
    std::set<int> s;
    while (n--) {
        int x;
        std::cin >> x;
        s.insert(x);
    }
    
    if (!s.count(1)) {
        std::cout << 0;
        return;
    }
    
    std::vector<bool> dp(*s.rbegin() + 1, 0);
    int ans = 0;
    
    for (auto &i : s) {
        std::set<int> p;
        int z = i;
        while (z != 1) {
            p.insert(si.mpf[z]);
            z /= si.mpf[z];
        }
        
        dp[i] = 1;
        
        for (auto &j : p) {
            dp[i] = dp[i] & dp[i / j];
        }
        
        ans += dp[i];
    }

    std::cout << ans;
}

F. tb的排列问题

题意

tb 给了 fc 一个长度为 $n$ 的排列 $A$ ,fc 想要更改一下此排列,他选择了一个幸运数字 $w$ ,依照如下方式操作了 $n$ 次:

第 $i$ 次操作中,选择两个整数 $x,y$,满足 $i≤x≤y≤min(i+w,n)$ ,交换 $A_x$​ 与 $A_y$​ 的值,$x = y$ 时数组不变。

经过这 $n$ 次操作后,他得到了新的排列 $B$。

由于 fc 的记忆只有七秒,他很快就忘记了 $A$ 排列是什么样子,只记得一部分 $A$ 排列中的元素以及其对应位置。现在他将 $B$ 排列以及残缺的 $A$ 排列给出,请你帮他计算一下一共有多少种可能的 $A$ 排列。(可能存在 fc 记忆错乱了,没有符合条件的 $A$ 。)

由于答案过大,输出方案数模以 $998244353$ 即可。

大概思路

直接正序遍历A数组模拟,一个个换B数组中各个数的位置。然后根据"当前遍历到的位置之前的数是无法再改变的",分情况讨论。

考虑两种情况:

  • 当前遍历到的数,在A数组中出现过
  • 当前遍历到的数,在A数组中没有出现过

那么对于出现的情况,我们可以这样考虑:

  • 如果出现的数的下标大于 $i + w$,那么显然不存在合法的原排列,因为你没法把那个数移动到当前位置。
  • 如果出现的数的下标小于 $i + w$,那么显然存在一种方式,将那个数移动到该位置:若其下标在 $i$ 到 $i + w$ 之间,可以直接换;若下标小于 $i$,那根据“当前遍历到的位置之前的数是无法再改变的”可知,这个数早在前面的步骤就被丢到后面来了。

对于没出现的情况,显然是要拿$-1$来顶,可以这样考虑:

  • 对于下标大于 $i + w$ 的 $-1$,我们不予考虑,因为类比之前的分析,我们知道这些 $-1$ 没法换过来。
  • 对于下标小于 $i + w$ 的 $-1$,显然是都有机会被放到当前位置的,我们只需要让答案乘上 “小于$i + w$的$-1$的个数 $-$ 已经使用过的 $-1$ 的个数”即可。

参考代码

constexpr int M = 998244353;

void solve() {
	int n, w;
    std::cin >> n >> w;
    
    std::vector<int> a(n), p = a, idx(n + 1, -1);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
        if (a[i] != -1) {
            idx[a[i]] = i;
        }
    }
    for (auto &i : p) std::cin >> i;
    
    i64 ans = 1;
    int used = 0;
    std::vector<int> pre(n);
    pre[0] = (a[0] == -1);
    for (int i = 1; i < n; i++) {
        pre[i] = pre[i - 1] + (a[i] == -1);
    }
    
    for (int i = 0; i < n && ans != 0; i++) {
        if (idx[p[i]] != -1) {
            if (idx[p[i]] > i + w) {
                ans = 0;
            }
        } else {
            (ans *= (pre[std::min(n - 1, i + w)] - used)) %= M;
            used++;
        }
    }
    std::cout << ans << "\n";
}