杂题 Part I

QOJ9564

考虑对于每次循环处理出,一个地方的袋鼠会走到哪个地方,连有向边,这样整个网格构成一棵内向基环树森林。
这样,每个点在第 \(i\) 个周期之后是否存在袋鼠都可以处理出来,也即内向基环树的每个点的最大深度。
然后再考虑周期内,直接考虑每一次合并两个相邻的格子的贡献,对于每一个操作,考虑 \((i, j)\) 表示我们已经处理出在不大于 \(h_{i, j}\) 的周期的当前操作前有袋鼠,然后我们由 \((a, b)\) 合并到 \((c, d)\),则会对 \([1, \min(h_{a, b}, h_{c, d})]\) 的周期的这一操作处产生贡献,且 \(h_{c, d} \gets \max(h_{c, d}, h_{a, b})\)。所以直接做就好了。

QOJ977

正常做我不会,考虑容斥,转化为至少有 \(x\) 个符合要求的位置,显然同一行或列不可能出现两个位置同时合法,不妨通过交换相邻的两行或列的操作,使得符合要求的位置集中在形如 \((i, i)\) 的位置上,且 \(\forall i \in [1, x - 1]\)\(a_{i, i} < a_{i + 1, i + 1}\),这一步会有 \(\frac{n!}{(n - i)!} \cdot \frac{m!}{(m - i)!}\) 的贡献。
然后先从 \((1, 1)\) 考虑,这样先插的数不会影响后插的数。
同时我们只考虑数的相对顺序,每次插入 \(n + m - 1, n + m - 3, \dots\) 个数,发现最大的数一定要排在所有数前面,剩下先插的数可以任意插,用插板法考虑即可,但是注意数的位置不同也要计入答案。
这样我们发现有 \(i\) 个合法位置的答案对于每个 \(1 \le j \le i\) 会被统计 \(\binom{i}{j}\) 次,我们需要钦定容斥系数使得 \(\sum_{j = 1}^i \binom{i}{j} d_j = [i = 1]\),打表发现 \(d_j = (-1)^{j - 1} j\),证明也是容易的,可以讨论 \(i\) 的奇偶性。

ARC112F

考虑这个东西形似进位,发现操作一轮只会令 \(c_1 \gets c_1 - (2^nn! - 1)\),并且我们总是能换则换,不妨把非第 \(1\) 个种类的牌倒换为第 \(1\) 个种类的牌。
所以可以把一个序列映射到一个数上,对于第 \(i\) 种卡包,我们设这个数设为 \(a_i\),初始的牌设为 \(P\),答案为 \(Q\),我们有:

\[P + \sum_{i = 1}^m a_ix_i \equiv Q \pmod {2^nn! - 1} \]

是一个不定方程的形式,这样我们得到了有解的充要条件是 \(\gcd(a_1, a_2, \dots, a_n, 2^nn! - 1) \mid (Q - P)\),令 \(f(X)\) 表示 \(c_1 = X\) 时的答案,前面的式子左边为 \(k\),则问题转化为 \(\min f((kx + P) \bmod (2^nn! - 1))\)
我们有两种算法:

  • 暴力枚举 \(x\),复杂度 \(O(n \cdot \frac{2^nn! - 1}{k})\)
  • 跑同余最短路,枚举放哪张牌,统计达到 \(P \bmod k\) 至少要多少张牌(相当于反凑出来),复杂度 \(O(nk)\)

考虑根号分治就可以了。

CF1647F

考虑序列的 \(\max\) 一定作为峰之一,设其在位置 \(i\),然后枚举另一个峰 \(j\),不妨令 \(i < j\),考虑 \(\le i\) 的部分两个序列都递增,可以扫 \(i\),贪心取另一个末位不在 \(i\) 的递增序列的末尾的数的 \(\min\)\(\ge j\) 的部分是类似的。
考虑中间的部分,其实也一样,只不过要加上当前位置是递增还是递减序列的最后一位,剩下的视情况取最小或最大的数。

P13346

考虑我们竟然只能传 \(1\) 个额外 bit,那么它除了决定第一次选择以外就没有用了。
如果我们下一步,把与第一轮未选择的点所有相邻的叶子问完,那么容易发现我们容易确定接下来选择的叶子,并扫掉了一个挂在树上的类菊花。
因此我们递归到了另一个子问题,这个时候我们只需要扫另一个与当前操作设计到的点不相关的菊花就可以了。
具体的,我们将当前刚被扫完菊花的点存进队列中,然后尝试检查它的父亲节点能不能形成新的类菊花,如果是就扫掉并加入优先队列,然后我们发现因为一棵树在不退化为菊花时,至少有两个这样的类菊花,所以自然能找到这样的点。
我们现在发现一个问题:一开始队列中的元素应当是哪些?发现所有可能的类菊花都应当被放进去,我们现在需要通过调整询问顺序来使得我们能够知道类菊花的根。
我们仍然采取“拿到类菊花就把把类菊花的叶子全部删完”的策略,转化为我们需要对一系列二元组调整顺序,使得扫过一个二元组的时候我们需要返回这个二元组对应的值(\(0/1\),表示应该选哪个点)。
我们把值为 \(0/1\) 的二元组分开,然后用二元组的大小来判断我们下一刻是否变化权值,于是我们把值相同的二元组排序之后试着两个顺序都拼接一遍,总有一个是对的。

CF1458D

先去刻画这个操作,注意到我们令 \(0 \to -1\) 之后取前缀和之后相当于选取一个前缀和相等的两个区间之间的部分然后反转。
不妨对于原来的相邻位置的前缀和 \(s_i, s_{i + 1}\),建边 \((s_i, s_{i + 1})\),问题转化为找到这个图 \(u \to v\) 的最小字典序欧拉路径(其实不大显然,但是我懒所以咕了)。
然后我们只需判断是否能从 \(x\)\(x - 1\),那么首先得能过去,其次若有右边的边能走回来接着走右边的边(其实可能也不大显然,但是我不会证),于是就做完了。

CF858F

又是我不会的典题捏。
考虑跑出来一棵树,然后只考虑非返祖边,如果边数为奇数,与任意一条返祖边匹配,否则边之间两两匹配,剩下的边留到祖先去处理。

P14196

我们发现,我们相当于给边定向,然后就会转化成,某些必须在 \(A\) 中出现,某些必须在 \(B\) 出现。
假设我们知道哪些在 \(A\) 中,哪些在 \(B\) 中,贡献系数只和 popcount 有关,容易计算。
发现限制形如:\(A, B\) 集合中不能与某个边无交,\(A \cup B\) 必须包含所有有边的点。
第一个限制随便处理,第二个限制发现固定一个之后另一个的所有可能形如一个集合的所有超集,于是高维后缀和做完了。

P14256

我们称一个人出的是石头、剪刀还是布为这个人的“状态”,首先我们观察操作的性质。
但是在介绍结论之前,我们尝试刻画平局。我们先钦定若干对人,使得对于每一对人 \((a, b, c)\)\(a, b\) 表示人的编号,\(c\) 表示两人的状态),我们应当合理操作使得某一时刻,我们会让他们对局并创造平局局面。
很显然有:

  • 任意两对 \((a_1, b_1, c_1), (a_2, b_2, c_2)\)\([a_1, b_1], [a_2, b_2]\) 两区间不能只相交不包含(不包括仅在边缘相交),因为要想让 \(a, b\) 对局,那么 \(a, b\) 之间的数必须被全部消除。
  • 如果方案中存在 \((a, b, c), (a, b', c)\) 两个不同的对局(\(a < b < b'\)),我们模拟对局过程后发现,可以调整为 \((a, b, c), (b, b', c)\)
  • 考虑 \((a_1, b_1, c_1)\),假设 \(c'\) 能被 \(c_1\) 打败,则我们令所有满足 \([a_2, b_2] \subseteq [a_1, b_1]\)\((a_2, b_2, c_2)\) 已经对局完毕(即消除完 \((a_2, b_2)\) 之间的数,注意此时 \(a_2, b_2\) 之一仍保留),那么剩余的 \((a_1, b_1)\) 之间的数必然存在 \(c'\),否则该方案不合法(当然有 \(b_i = a_i + 1\) 的特殊情况,我们称这样的限制为“对区间 \(X\) 上”的)。

因此我们有如下结论:
结论 \(1\):若当前存在两个相邻的人(设为 \(u, u + 1\))状态相同,必然让这两个人对局。
考虑一种方案不满足上述结论,\(u, u + 1\) 之一若有不超过一个和外面对局产生平局,显然调整会使得限制条件变松,若有 \((a, u, c), (u + 1, b, c)\) 两个对局,可调整为 \((a, b, c), (u, u + 1, c)\),发现 \((a, b)\) 这个区间的限制中,原来限定有 \(2\)\(c'\),现在限定只有 \(1\) 个,限制还是变松了。
结论 \(2\):若当前存在三个相邻的人(设为 \(u - 1, u, u + 1\)),满足 \(u - 1, u + 1\) 状态相同,且都能打败 \(u\),必然让 \(u - 1, u + 1\) 对局。
仍考虑一种方案不满足上述结论,对于 \(u - 1, u + 1\) 的不能向外对局的部分,可以参照上文。
我们考虑 \(u\) 能否向外对局,发现 \(u\) 若对平局有 \(1\) 的贡献,那么直接把对局变成 \((u - 1, u + 1, c)\) 不劣。
否则我们发现,设有 \((a, u, c), (u, b, c)\) 两个对局,直接调整为 \((a, b, c), (u - 1, u + 1, c)\),与上文同理,此处限制仍然变松(注意 \((a, u), (u, b)\) 之间一定有不同于 \(c, c_{u \pm 1}\) 的状态,)。
于是我们模拟一个栈,一次加入一个数,按上述规则消除,会发现:

  • 最终序列形如,存在一个分割点 \(x\),使得从 \(x\) 开始向左,每相邻的一对必有右边的人能打败左边的人;从 \(x\) 开始向右,每相邻的一对必有左边的人能打败右边的人。

我们不妨考虑,可以让所有某一个状态的人与下一个状态相同的人对局,这样由于序列的循环性,每个区间的限制都能满足(注意与 \(x\) 相邻的两个人不能够互相对局产生平局)。
假设 \(x\) 是从前往后数第 \(a\) 个,从后往前数第 \(b\) 个,发现答案即为 \(\lfloor \frac{a - 1}{3} \rfloor + \lfloor \frac{b - 1}{3} \rfloor + [(a \bmod 3 = 0) \land (b \bmod 3 = 0)]\)\([X]\) 表示艾弗森括号。
为方便理解,下面给出计算一个序列的问题答案的 Calc 函数:

int Calc(int n, int *b) {
    int i, k = 0, res = 0, col = 0;
    for(i = 1; i <= n; i++) {
        if(k && c[k] == b[i]) res++;
        else if(k > 1 && beat[b[i]][c[k]] == 1 && beat[c[k - 1]][c[k]] == 1) res++, k--;
        else c[++k] = b[i];
    }
    for(i = 2; i < k; i++) if(beat[c[i]][c[i - 1]] == 1 && beat[c[i]][c[i + 1]] == 1) break;
    return res + (i - 1) / 3 + (k - i) / 3 + (i % 3 == 0 && (k - i + 1) % 3 == 0);
}

b 表示每个人的状态,c 是一个栈,beat[x][y] 表示 \(x\) 能否战胜 \(y\),能是 \(1\),不能是 \(-1\),平局是 \(0\)
我们可以用 \((a, b, c)\) 表示当前栈序列,其中 \(c\) 是第一个人的状态,\(a, b\) 意义如上文所述,就可以 dp 了。
具体的,我们可以算出当前栈最后一个人的状态 \(c'\),我们将 \(b\)\(1\) 比较,可以获知栈中倒数第二个人的状态,模拟上文过程就可以了,一个经典 dp 方式是用两个数组分别记录方案数和答案,这样的复杂度是 \(O(n^3)\)
进一步的,我们只关心 \(\lfloor \frac{a - 1}{3} \rfloor\) 的值,这个可以在 \(a\) 变化时动态计算贡献,而我们在计算 \(c'\) 时只关心 \(a \bmod 3\) 的值,因此在 dp 状态里,我们只需要 \(a \bmod 3\) 的值,复杂度降为 \(O(n^2)\)

#include <bits/stdc++.h>
#define ll long long
using namespace std;
inline ll Read() {
    int sig = 1; ll num = 0; char c = getchar();
    while(!isdigit(c)) { if(c == '-') sig = -1; c = getchar(); }
    while(isdigit(c)) num = (num << 3) + (num << 1) + (c ^ 48), c = getchar();
    return num * sig;
}
void Write(ll x) {
    if(x < 0) putchar('-'), x = -x;
    if(x > 9) Write(x / 10);
    putchar((x % 10) ^ 48);
}
const int N = 3005;
const ll Mod = 1e9 + 7;
int n, a[N], beat[3][3] = {{0, -1, 1}, {1, 0, -1}, {-1, 1, 0}};
ll f[2][3][N], g[2][3][N];
void Add(ll &x, ll y) { x += y; if(x >= Mod) x -= Mod; }
ll DP(int c) {
    int o = 0, i, j, k, l; ll ans = 0;
    memset(f, 0, sizeof(f)), memset(g, 0, sizeof(g)), f[o][1][0] = 1;
    for(i = 2; i <= n; i++, o ^= 1) {
        for(j = 0; j < 3; j++) for(k = 0; k < i; k++) if(f[o][j][k]) {
            int x = (c + j - 1 - k + 3000) % 3;
            for(l = 0; l < 3; l++) if(a[i] & (1 << l)) {
                if(x == l) Add(f[o ^ 1][j][k], f[o][j][k]), Add(g[o ^ 1][j][k], g[o][j][k]), Add(g[o ^ 1][j][k], f[o][j][k]);
                if(beat[x][l] == 1) Add(f[o ^ 1][j][k + 1], f[o][j][k]), Add(g[o ^ 1][j][k + 1], g[o][j][k]);
                if(beat[x][l] == -1) {
                    if(k) Add(f[o ^ 1][j][k - 1], f[o][j][k]), Add(g[o ^ 1][j][k - 1], g[o][j][k]), Add(g[o ^ 1][j][k - 1], f[o][j][k]);
                    else {
                        Add(f[o ^ 1][(j + 1) % 3][k], f[o][j][k]), Add(g[o ^ 1][(j + 1) % 3][k], g[o][j][k]);
                        if(j == 0) Add(g[o ^ 1][(j + 1) % 3][k], f[o][j][k]);
                    }
                }
            }
            f[o][j][k] = g[o][j][k] = 0;
        }
    } 
    for(i = 0; i < 3; i++) for(j = 0; j <= n - i; j++) Add(ans, g[o][i][j]), Add(ans, f[o][i][j] * (j / 3 + (j % 3 == 2 && i % 3 == 0)) % Mod);
    return ans;
}
int main() {
    // freopen("draw.in", "r", stdin), freopen("draw.out", "w", stdout);
    int i; ll ans = 0; string s; cin >> n >> s;
    for(i = 1; i <= n; i++) a[i] = s[i - 1] - '0';
    if(a[1] & 1) Add(ans, DP(0));
    if(a[1] & 2) Add(ans, DP(1));
    if(a[1] & 4) Add(ans, DP(2));
    Write(ans);
}

ARC198E

考虑有 dp:

\[f_i = \sum_{j \ | \ s_k = i - 1} f_j \]

发现转移数太大了,假设我们已知 \(f_{1 \sim i - 1}\),我们考虑求后面的东西,发现可以容斥,用 \(j \ | \ (i - 1) = i - 1 \land s_k \ | \ (i - 1) = i - 1\) 的方案数减去 \(j \ | \ s_k < i - 1 \land j \ | \ s_k \ | \ (i - 1) = i - 1\) 的方案数,两个都恰好可以表示成所有是 \(i - 1\) 的真子集 \(j\)\(f_j\)\(f_{j + 1}\) 之和。
于是就转化为了动态在末尾添加数,动态查询末位置的卷积的问题,考虑分治,左区间对右区间的贡献可以在开头减去区间外的贡献,在左区间递归结束之后,左区间的数自然的就被卷上去了,加回去就可以了。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
inline ll Read() {
    int sig = 1; ll num = 0; char c = getchar();
    while(!isdigit(c)) { if(c == '-') sig = -1; c = getchar(); }
    while(isdigit(c)) num = (num << 3) + (num << 1) + (c ^ 48), c = getchar();
    return num * sig;
}
void Write(ll x) {
    if(x < 0) putchar('-'), x = -x;
    if(x > 9) Write(x / 10);
    putchar((x % 10) ^ 48);
}
const int N = 24;
const ll Mod = 998244353;
int n, m; ll cnt[1 << N], f[(1 << N) + 5], g[1 << N], p[1 << N], q[1 << N];
void Add(ll &x, ll y) { x += y; if(x >= Mod) x -= Mod; }
void Solve(int l, int r) {
    if(l + 1 == r) { Add(p[l], f[l]), f[l + 1] = g[l] = (cnt[l] * p[l] - q[l] + Mod) % Mod, Add(q[l], g[l]); return ; }
    int i, mid = (l + r) >> 1;
    for(i = l; i < mid; i++) Add(p[i + mid - l], Mod - p[i]), Add(q[i + mid - l], Mod - q[i]);
    Solve(l, mid);
    for(i = l; i < mid; i++) Add(p[i + mid - l], p[i]), Add(q[i + mid - l], q[i]);
    Solve(mid, r);
}
int main() {
    int i, j; n = Read(), m = Read(); while(m--) cnt[Read()]++;
    for(i = 0; i < n; i++) for(j = 1; j < (1 << n); j++) if((j >> i) & 1) Add(cnt[j], cnt[j ^ (1 << i)]);
    f[0] = 1, Solve(0, 1 << n), Write(f[1 << n]);
}
posted @ 2025-08-31 19:58  Include_Z_F_R_qwq  阅读(28)  评论(2)    收藏  举报