25-12

CF

CF-tag-dp

CF2170E *2100

低烧了又如何?

先考虑怎样判断一个 01 串是否是美丽的。

  • 如果原串存在奇数个块:
    • 若块数 \(>1\),则显然可以删掉中间的一个块,达到减少两个块的效果,所以是美丽的。
    • 若块数 \(=1\),则删掉一个块后原串变为空串,所以不美丽。
  • 如果原串存在偶数个块,则删掉旁边的一个块即可达到减少一个块的效果,所以是美丽的。

也就是说题目想让我们求的是对于每一个 \(i\),满足 \([l_i, r_i]\) 中至少包含两个块的 01 串个数。

此时考虑以 \(r_i\) 为第一关键字,\(l_i\) 为第二关键字从小到大排序,然后设计一个 dp 来求解问题。不妨钦定第一个位置放 \(0\),最后再给答案 \(\times 2\) 即可。

\(f_i\) 表示前 \(i\) 个位置的方案数,\(lst_i\) 表示在 \(i\) 之前最近的 \(l\) 的位置,转移如下:

\[f_i\leftarrow \sum_{j=lst_i}^{i-1} f_j \]

用一个前缀和优化容易做到 \(O(n)\),最后答案即为 \(f_n\times 2\)

CF2150C *2100

考虑对于 \(a\) 的每一个子集,哪些能选到,哪些不能选到。发现一个性质,对于一个数 \(x\),若 \(x\) 能存在于子集 \(S\) 中,则必须满足在 \(a\)\(x\)\(S\) 中的离它最近的数 \(y\) 中间的所有数在 \(b\) 中均出现于 \(x\) 之前。然后有一个暴力的 dp,设 \(f_i\) 表示 dp 了 \(a\) 中的前 \(i\) 位的最大值,转移为:

\[f_i=\max\limits_{j<i}(f_{i-1}, f_j+v_i) \]

其中的 \(j\) 需要满足如上所述的条件。完了,我好菜,暴力 dp 都不会。。。

一维 dp 不好搞,再加一维,设 \(f_{i, j}\) 表示当前在 \(a\) 中枚举到 \(i\) 且在 \(b\) 中枚举到 \(j\) 的最大值。转移是平凡的:

\[f_{i, j}\leftarrow\max\{f_{i,j-1}, f_{i-1,k}+[k<pos_{a_i}]\times v_{a_i}\} \]

\(pos_{a_i}\) 表示的就是 \(a_i\)\(b\) 中的位置。

发现这个东西可以滚动数组,然后就等同于区间加和全局最大值了,可以上个线段树优化转移。什么傻逼 WA!!!

CF2176D *1800

耻辱啊!我的图论!

发现如果是 DAG 的话,dp 是非常容易的,但是如果在有环图上就是困难的。Tarjan 缩点的显然不是很可做。于是发现斐波那契数列具有一个单调性,那就是先从 \(a_u+a_v\) 大的边向 \(a_u+a_v\) 小的边转。然后排一个序,dp 转移就行了。

CF2165B *1900

想到的:注意到数据范围是小的,所以可以考虑一下 dp。感觉没什么性质哎,应该可以枚举每一种数在 \(s\) 中数量。哦,有一个显然的结论,如果 \(x\)\(s_i,s_{i+1},\cdots,s_{i+k}\) 中出现了,那么 \(x\) 显然可以只出现在这些集合中。假设 \(s\) 的大小为 \(|s|\)。那么如果数 \(x\) 想要在 \(s\) 中出现 \(d\) 次,就必须满足存在 \(d\) 个集合的大小满足 $\sum_{i}^{i+d} s_i\le 2\times\operatorname{number\ of\ }x $。由上面那一个显然的结论,现在就只需要考虑对于每一个 \(x\) 不存在于 \(s\) 中的情况。也就是说,只有在不出现的数的个数之和 \(\le \lfloor\frac{|s|}{2}\rfloor\) 时才是可行的,然后剩下的数就可以随便分了。设存在的数 \(i\) 的出现个数为 \(cnt_i\),一种情况对答案的贡献是:

\[\prod_{i} cnt_i \]

于是乎现在只需要求出有多少种本质不同的方案使一些数不存在于 \(s\) 中。发现这个东西是一个背包啊!但是最后你怎么计算答案呢?能不能再预处理一个有 \(k\) 个数存在于 \(s\) 中的方案数,然后统计时相乘?但是这样子需要保证一种数不能同时存在又不存在。

很好,上面的结论是假的。妈的 fuck。又想到一个正宗的结论,那就是出现在 \(s\) 中的数的出现次数之和要 \(\ge\) 未出现在 \(s\) 中的数的出现次数之和的最大值。

记出现在 \(s\) 中的数的次数之和为 \(cnt1\),未出现在 \(s\) 中的数的次数之和为 \(cnt1\),未出现的数的出现次数最大值为 \(maxn\),则有合法情况为:

\[cnt1\ge maxn\\ \therefore n-cnt1\le n - maxn\\ \therefore cnt0\le n - maxn \]

这个结论很漂亮。受题解启发, 我们按照出现次数拍一个序,然后枚举每一个数作为 \(maxn\) 时候的贡献。考虑对这一个东西进行 dp,和上面想的一样,仍然是一个背包,设 \(f_{i, j}\) 表示现在 dp 到 \(i\) 且未出现在 \(s\) 中的数的出现次数之和为 \(j\)。设 \(cnt_i\) 表示 \(i\) 出现的次数,考虑转移(怎么是乘法分配律,我上面怎么没细想……):

\[f_{i, j}\leftarrow f_{i-1, j} + f_{i-1,j-cnt_i}\div cnt_i \]

初始值是 \(f_{0, 0}=\prod_{i=1}^{n} cnt_i\)

CF2161D *2100

想到的: 观察到 \(a_i\in [1, n]\),这启示我们从 \(1\)\(n\) 枚举数,然后 dp。现在我们考虑所有的满足 \(a_i=x\)\(a_i\),看能否发现一些性质。设这些 \(a_i\) 组成一个新的集合 \(b\)\(f_{i,1/0}\) 表示 dp 到 \(b_i\)\(b_i\) 删或不删的最小花费。转移是容易的:

  • \(f_{i, 1}=\min\{f_{i-1, 0}, f_{i-1,1}\}+1\)
  • \(f_{i, 0}=\min_{j<i}\{f_{j, 0} + \operatorname{contribution}\}\)

其中,\(\operatorname{contribution}\) 就是需要删除的 \(x-1\) 的个数。设 \(pos_i\)\(b_i\)\(a\) 中的下标。则有 \(\operatorname{contribution}\) 为在 \([pos_j,pos_i]\) 中的 \(x-1\) 的个数。这个东西是可前缀和优化成 \(O(n)\) 的。

现在需要考虑如何在 \(x\)\(x+1\) 之间转移。但是这样似乎就有后效性了。。。

转化一下问题,给每一个数赋权为满足 \(j<i\)\(a_i-a_j=1\) 的数的个数。然后你可以选择一个数 \(a_i\) 使得所有 \(j<i\)\(a_j=a_i-1\) 的数的权值 \(-1\),所有 \(j>i\)\(a_j=a_i+1\) 的数的权值 \(+1\),同时该数的权值变成 \(0\)。需要使最后每一个数的权值都为 \(0\),问最少操作次数。 这个转化屁用没有。

考虑正难则反,从 \(a\) 中选择最多的数使得满足题意。添加一个 \(x\) 至选择的数列的末尾的条件是数列中不存在 \(x-1\)。设 \(f_i\) 表示 dp 到 \(a_i\) 时的答案,朴素的转移是容易的。但是很难判断 \(x-1\) 是否存在于数列中。尝试使用线段树维护当前数列中不存在 \(x-1\) 的最长子序列,然后以此进行转移。感觉很重要的一个东西是 dp 数组单调不降(假了)。可是似乎需要树套树才能实现,而且相当复杂……

考虑从小到大加入数,然后进行 dp。但是这是不充分的,因为可能出现从大到小的部分。所以是否需要从大到小再加入一遍数,最后进行合并的 dp。这样如何合并?

没想到的:发现直接 dp 不好搞,所以对 \(a\) 进行升序排序,然后设 \(f_i\) 表示前 \(i\) 个数的最大保留长度,然后考虑转移。发现这个东西的转移是容易的,设 \(pos_i\) 表示 \(i\) 在原数组中的下标,则:

\[f_i=\max\limits_j^{b_j<b_i-1\vee pos_j>pos_i}\{f_j+1\} \]

于是乎你只需要用数据结构维护这个东西就行了。哎,我傻了……

CF2174B *1900

还是补赛时没过的 dp。

想到的:观察到只有单调上升的数才会有贡献,于是先把这些数提出来。我赛时观察到了这个,但是没发现只有 \(k\) 个这样的数,于是……接下来就可以 \(k^3\) 过了,所以 dp。设计状态为 \(f_{i, j, k}\) 表示当前 dp 到第 \(i\) 位,且当前最大值为 \(j\)、还剩 \(k\) 张可用卡片的答案。考虑一个朴素的转移:

  • \(a_{i+1} > j\)\(a_{i+1}\le k\),则:

\[f_{i+1, a_i, k-a_i}=\max\{f_{i, j, k} + (dis(a_i, a_{i+1})-1)\times j+a_i\} \]

  • \(a_{i+1}<j\),则:

\[f_{i+1, j, k}=\max\{f_{i, j, k}\} \]

这个东西的时间复杂度是 \(O(k^4)\),但是观察到可以前缀最大值优化,所以应该是可以做到 \(O(k^3)\)。具体来说,\(f_{i+1,a_i,k-a_i}=\max\{f_{i, j, k} + (dis(a_i, a_{i+1})-1)\times j\}+a_{i+1}\)。所以直接使用前缀最大值就行了。不过需要开二维,记录一个 \(k\)

CF2173D *1900

烦死了,因为 C 导致这题没时间了。

想到的:手玩样例发现,在进行了 \(\log_2 n\) 次操作之后,每次操作最多便只能增加 \(1\) 的贡献,这启示我们对于前 \(\log_2 n\) 次操作进行单独计算。注意到这东西很小,所以考虑 dp。但是我赛时把 dp 给否掉了。。。因为是从低位往高位进位,所以从低位向高位 dp。设 \(f_{i, j, k}\) 表示进行 \(i\) 次操作,枚举完前 \(j\) 位,上一个 \(0\) 的位置为 \(k\) 的答案。考虑转移。

  • \(k<j\),则:\(f_{i, j, k}=\max\{f_{i-1,j,k+1}+\max \{f_{i-1,k, k_1} + k - k_1\}\}\)
  • \(k=j\),则:\(f_{i, j, k}=\max\{f_{i, j-1,k_1}\}\)

发现这东西是可以优化的,首先可以滚动数组去掉一维,然后 \(\max \{f_{i-1,k, k_1} + k - k_1\}\) 这个东西可以在每次 dp 完之后处理,去掉一个 \(\log_2 n\)。这样子的复杂度就应该是 \(O(\sum \log_2^3 n)\)

这个东西假了?确实,不能很好的表示状态……

没想到的:有一个转化为:

\[ans\leftarrow \operatorname{popcount}(n)+k-\operatorname{popcount}(n') \]

\(n'\) 为操作完后的 \(n\)。,所以我们想要最小化操作完的 \(\operatorname{popcount}\)。于是状态设成 \(f_{i, 0/1}\) 表示 dp 到第 \(i\) 位且该位是否从上一位进位。然后因为对于每一位可以选择 \(+1\) 也可以不选,所以转移是容易的,判一下进位就行了。这个的时间复杂度是 \(O(\sum \log_2^2 n)\) 的。

杂题

CF2178E *?

注意到如果没有操作二,那么 \(a\)\(b\) 中的数的总和都不会变化。如果存在操作二会怎么样?相当于把 \(a\)\(b\) 中的数的总和都 \(\times 2\)。因为 \(n\le 10^5\),所以拼接操作不会超过 \(17\) 次,也就是说 \(300\) 允许我们 \(\log^2 n\) 次询问通过。所以能否考虑把所有的拼接操作的位置都二分出来?

注意到每次拆分操作都只会对最大值进行,所以这有什么启发意义?很难搞,由于拼接操作的存在,\(2^k\)\(2^{k-x}(x\ge 2)\) 就可能同时出现,这种情况是难以计算答案的。如果不会出现这种情况,则直接判断子数组长度与和的关系即可得到最大值。

猜一个结论,对于二分出来的两个子数组,其和相等,则最大值一定在长度短的那一个子数组中。易证。

CF2178D *?

感觉这个东西图论建模的话似乎比较好做。观察到每次精灵的攻击操作相当于选择一个入度为 \(0\) 的节点,然后向一个点权为正的点连有向边。注意到在选择的 \(a_i\)\(a_j\) 互不相同的情况下,每次攻击操作后必然会有一个精灵死亡,并且我们可以通过从小到大进行操作避免出现 \(a_i=a_j\) 的情况的发生。所以策略便是贪心地从小到大匹配。

现在的问题变成怎样让最后剩 \(k\) 个精灵。这相当于最后 \(k\) 个精灵都至少发起过一次攻击。所以 \(k\in [0, \frac{n}{2}]\),然后就简单了。

可是如果 \(k=0\) 就比较难搞。等会,这是不是可以用一个大根堆模拟?每次取最大的两个消掉,这样贪心应该是对的。

完了,我发现一个很重要的东西,那就是我读题读错了,\(a_i\) 恒定且每一个精灵只能攻击一次别人!哦,那你直接排个序,然后找到一段连续的小的攻击最大的即可。

CF2153F *2900

绷不住了,简单分块可过的 *2900。

考虑预处理一下第 \(l\) 个块到第 \(r\) 个块的答案,然后再处理一个第 \(i\) 个块到最后一个块的每一个数的出现个数就行了。接下来整块直接查答案,散块暴力判断出现次数即可。

CF2168B *?

来做一做通信题。

发现从 \(x\) 不好入手,于是考虑从第二个人的角度来思考。观察到 \(30>2\times \log_2(10^4)\),所以可以二分的询问,并且可以这样执行两组。经过一顿瞪眼可得,在一个排列中最大值减最小值的差是唯一的,我们抓住这个唯一性便可以在两组二分中找到最大值和最小值的位置。此时只需要通过 \(x\) 判断最大值在最小值的左边还是右边就行了。

CF2154C2 *2000

想到的:首先肯定不会操作超过两次,因为两次一定可以凑出两个偶数。所以说可以先给所有数按 \(b_i\) 排序,然后将最小的两个 \(b_i\)\(a_i\) 变成偶数所需的花费为备选答案。然后还有一个观察,那就是按 \(a\) 从小到大排完序以后每一个 \(a_i\),最多只可能变成 \(a_{i+1}\)。所以此时一个考虑是每次查询 \([a_i, a_{i + 1}]\) 中有没有 \(a_j(j\in [1, n])\) 的因数,如果有就选取最靠近 \(a_i\) 的。但这个东西似乎不充分。并非,这个东西似乎已经很充分了,待我实现一番。问题来了,为什么 \(\gcd(a_i, a_j)=a_i\),这东西不一定成立。想到了用调和级数搞因数加 set 维护的 \(O(\sum n\sqrt{V}+n\log^2 n+\tau(V)\times n\log^2 n)\) 做法,码量大,常数也大……应该过不了。

没想到的:观察到只可能对于 \(b_i\) 最小的数进行多次操作。于是考虑枚举质因数,判断最小操作次数。

哎,就差一点……放个代码吧。

::::success[AC Code]

#include <bits/stdc++.h>
using namespace std;

using ll = long long;
using ld = long double;
using ull = unsigned long long;
using i128 = __int128;

using PII = pair<int, int>;
using PLL = pair<ll, ll>;

constexpr ll inf = (1ll << 62);
constexpr int N = 4e5;

struct Node {
	ll a, b;
};

struct custom_hash {
	static uint64_t splitmix64(uint64_t x) {
		x += 0x9e3779b97f4a7c15;
		x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
		x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
		return x ^ (x >> 31);
	}
	
	size_t operator()(uint64_t x) const {
		static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
		return splitmix64(x + FIXED_RANDOM);
	}
};


void solve() {
	int n;
	cin >> n;
	vector<Node> d(n);
	unordered_map<ll, int, custom_hash> tot;

	auto add = [&](int x, int val) -> void {
		for (int i = 2; i * i <= x; i++) {
			if (x % i == 0) {
				tot[i] += val;
				if (!tot[i]) {
					tot.erase(i);
				}
				while (x % i == 0) {
					x /= i;
				}
			}
		}
		if (x != 1) {
			tot[x] += val;
			if (!tot[x]) {
				tot.erase(x);
			}
		}
	};

	auto check = [&](int x) -> bool {
		for (int i = 2; i * i <= x; i++) {
			if (x % i == 0) {
				if (tot.count(i)) {
					return true;
				}
				while (x % i == 0) {
					x /= i;
				}
			}
		}
		if (x != 1) {
			if (tot.count(x)) {
				return true;
			}
		}
		return false;
	};

	for (int i = 0; i < n; i++) {
		cin >> d[i].a;
		add(d[i].a, 1);
	}
	for (int i = 0; i < n; i++) {
		cin >> d[i].b;
	}
	sort(d.begin(), d.end(), [&](Node x, Node y) {
		return x.b < y.b;
	});
	add(d[0].a, -1);
	ll ans = d[0].b + d[1].b;
	for (auto [x, y] : tot) {
		ans = min(ans, (x - d[0].a % x) % x * d[0].b);
	}
	add(d[0].a, 1);
	for (int i = 1; i < n; i++) {
		add(d[i].a, -1);
		if (check(d[i].a)) {
			ans = min(ans, 0ll);			
		}
		if (check(d[i].a + 1)) {
			ans = min(ans, d[i].b);
		}
		add(d[i].a, 1);
	}
	cout << ans << "\n";
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t = 1;
	cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

::::

AT

abc419f

做 ACAM。

注意到数据范围很小,因此先想到一个暴力的 dp。但是如果只是普通的 dp,表达不了现在的字符串的模样,因此考虑在 trie 或 ACAM 上 dp,又因为需要表示子串之间的包含关系,所以选择 ACAM。

分析 dp 的状态。首先肯定有一维表示的是现在 dp 到第几个字符。然后你观察到 \(n\) 很小,所以可以考虑在处理 fail 时状压一下把每一个字符串是否出现搞出来。于是状态似乎就呼之欲出了:

\(f_{i, j, k}\) 表示 dp 到 ACAM 上的节点 \(i\)、此时枚举到字符串的第 \(j\) 位,状态为 \(k\) 的方案数。转移是简单的:

\[f_{i, j, k\operatorname{OR} \operatorname{state\ of}i}=\sum f_{fa_i, j-1,k} \]

初始值怎么赋?\(f_{0, 0, 0} = 1\)。需要注意的是每一个节点后面可以跟不存在的子节点,所以需要略微调整一下转移,即通过枚举子节点来转移。

abc429f

看了题解才会维护。。。非常妙的 trick。

发现行数很少,所以一次改变所带来的影响是有限的且在一段区间内,因此考虑用线段树。对于列 \([l, r]\),记 \(f(i, l, j, r)\) 表示从 \((i, l)\) 走到 \((j, r)\) 的最少步数,然后便有转移:

\[f(i, l, j, r)=\min\{f(i, l, k, mid)+f(k, mid + 1, j, r) + 1\} \]

观察到这个式子其实等同于线段树的合并操作,于是启发我们用线段树来维护。线段树的每一个节点包含的区间就代表 \([l, r]\) 这些列的答案,再记录一个 \(d_{i, j}\) 表示上述 \(f\) 即可。合并是平凡的。

时间复杂度:\(O(q\log n)\)。带个合并的 \(27\) 的常数。

LG

dp

P14666 \(\color{green}\text{绿}\)

想到的:

观察样例,猜一个结论:游走的终点只可能是节点 \(1\)。考虑证明,一个节点如果往儿子走最终显然是可以再走回父亲的,但如果走到了父亲就不能再走回去了。所以只有走到一个没有父亲的点且把这个点的所有子树都走遍才会停止。这里的走遍指至少在这个子树中走过了一个节点。

得出这个结论之后,问题便转化为 \(1\)\(s\) 的路径必走,同时可以走到其它节点,问走过本质不同的路径长度之和。因为 \(s\)\(1\) 的路径只会走一遍,而其它的存在与路径上的点都需要走两遍,设路径总数为 \(num\)、其它存在于路径上的点的路径长度之和为 \(len\),则答案便等于:\(dis(1, s)\times num+2\times len\)

考虑通过树形 dp 计算出 \(num\)\(len\)。发现直接对整棵树进行 dp 并不容易,但如果 \(s=1\) 是很好计算的,于是想到拆贡献,最后再合并。可以先把 \(1\)\(s\) 的路径上的边删掉,然后此时原树变成了一个森林,考虑对于每一个森林中的树进行树形 dp。对于每一个树都钦定在 \(1\)\(s\) 的路径上的点 \(u\) 为根,然后每一棵树就等同于跑一遍 \(s=u\) 的树形 dp,这个东西可以直接套用 \(s=1\) 的情况。

\(f_u\) 表示以 \(u\) 为根的子树中的合法路径长度之和,\(g_u\) 表示以 \(u\) 为根的子树中的合法路径数量。

  • 如果 \(u\neq 1\),则每一个子树可以选择走和不走,所以:

\[g_u=\prod_{v\in son_u}(g_v+1) \]

  • 如果 \(u=1\),则每一个子树都必须走,所以:

\[g_u=\prod_{v\in son_u}g_v \]

接下来考虑 \(f\) 的转移。在 \(u\) 的子树中,\(f_v(v\in son_u)\) 可能和其它的 \(u\) 的儿子一起产生贡献,所以只需要知道其它儿子的组合的方案数就可以算出 \(f_u\)。同时,因为从 \(v\)\(u\) 也会产生 \(1\) 的贡献,这个贡献的前提是 \(v\) 必选,其它的儿子可以任意组合。

  • 如果 \(u\neq 1\),则每一个子树可以选择走和不走,转移为:

\[f_u=\sum_{v\in son_u} (f_v+g_v)\times g_u\div (g_v+1) \]

  • 如果 \(u=1\),则每一个子树都必须走,转移为:

\[f_u=\sum_{v\in son_u} (f_v+g_v)\times g_u\div g_v \]

最后再把 \(1\)\(s\) 的路径上的点合并即可。因为路径上的每一个点都必须选,所以是按照 \(u=1\) 的转移方式合并的。

时间复杂度:\(O(n\log V)\)。要特判一下 \(s=1\) 的情况。


做法死了,因为可能没有逆元,所以考虑在转移的时候对每一个节点都做一个前后缀积,这样可以避免逆元的使用。此时的时间复杂度严格线性。

::::success[Code]

#include <bits/stdc++.h>
using namespace std;

using ll = long long;
using ld = long double;
using ull = unsigned long long;
using i128 = __int128;

using PII = pair<int, int>;
using PLL = pair<ll, ll>;

constexpr ll inf = (1ll << 62);
constexpr int N = 1e6 + 10;

template <typename T>
concept Can_bit = requires(T x) { x >>= 1; };
template <int MOD>
struct modint {
    int val;
    static int norm(const int& x) { return x < 0 ? x + MOD : x; }
    static constexpr int get_mod() { return MOD; }
    modint inv() const {
        assert(val);
        int a = val, b = MOD, u = 1, v = 0, t;
        while (b > 0) t = a / b, swap(a -= t * b, b), swap(u -= t * v, v);
        assert(b == 1);
        return modint(u);
    }
    modint() : val(0) {}
    modint(const int& m) : val(norm(m)) {}
    modint(const long long& m) : val(norm(m % MOD)) {}
    modint operator-() const { return modint(norm(-val)); }
    bool operator==(const modint& o) { return val == o.val; }
    bool operator<(const modint& o) { return val < o.val; }
    modint& operator+=(const modint& o) { return val = (1ll * val + o.val) % MOD, *this; }
    modint& operator-=(const modint& o) { return val = norm(1ll * val - o.val), *this; }
    modint& operator*=(const modint& o) { return val = static_cast<int>(1ll * val * o.val % MOD), *this; }
    modint& operator/=(const modint& o) { return *this *= o.inv(); }
    modint& operator^=(const modint& o) { return val ^= o.val, *this; }
    modint& operator>>=(const modint& o) { return val >>= o.val, *this; }
    modint& operator<<=(const modint& o) { return val <<= o.val, *this; }
    modint operator-(const modint& o) const { return modint(*this) -= o; }
    modint operator+(const modint& o) const { return modint(*this) += o; }
    modint operator*(const modint& o) const { return modint(*this) *= o; }
    modint operator/(const modint& o) const { return modint(*this) /= o; }
    modint operator^(const modint& o) const { return modint(*this) ^= o; }
    modint operator>>(const modint& o) const { return modint(*this) >>= o; }
    modint operator<<(const modint& o) const { return modint(*this) <<= o; }
    friend std::istream& operator>>(std::istream& is, modint& a) {
        long long v;
        return is >> v, a.val = norm(v % MOD), is;
    }
    friend std::ostream& operator<<(std::ostream& os, const modint& a) { return os << a.val; }
    friend std::string tostring(const modint& a) { return std::to_string(a.val); }
    template <Can_bit T>
    friend modint qpow(const modint& a, const T& b) {
        assert(b >= 0);
        modint x = a, res = 1;
        for (T p = b; p; x *= x, p >>= 1)
            if (p & 1) res *= x;
        return res;
    }
};
using M107 = modint<1000000007>;
using M998 = modint<998244353>;

constexpr int mod = 1e9 + 7;

using Mint = M107;
// constexpr mod = ...;
// using Mint = modint<mod>;
struct Fact {
    std::vector<Mint> fact, factinv;
    const int n;
    Fact(const int& _n) : n(_n), fact(_n + 1, Mint(1)), factinv(_n + 1) {
        for (int i = 1; i <= n; ++i) fact[i] = fact[i - 1] * i;
        factinv[n] = fact[n].inv();
        for (int i = n; i; --i) factinv[i - 1] = factinv[i] * i;
    }
    Mint C(const int& n, const int& k) {
        if (n < 0 || k < 0 || n < k) return 0;
        return fact[n] * factinv[k] * factinv[n - k];
    }
    Mint A(const int& n, const int& k) {
        if (n < 0 || k < 0 || n < k) return 0;
        return fact[n] * factinv[n - k];
    }
};

int n, s;
vector<vector<int>> G(N);
vector<Mint> f(N), g(N);
vector<int> depth(N), path;
bitset<N> flag;

bool dfs1(int u, int fa) {
	bool ok = false;
	if (u == s) {
		ok = true;
	}
	for (auto v : G[u]) {
		if (v == fa) continue;
		depth[v] = depth[fa] + 1;
		ok |= dfs1(v, u);
	}
	if (ok) {
		path.push_back(u);
	}
	return ok;
}

void dfs2(int u, int fa) {
	g[u] = 1;
	vector<Mint> pre(int(G[u].size())), suf(int(G[u].size()));
	for (auto v : G[u]) {
		if (v == fa || flag[v]) continue;
		dfs2(v, u);
		g[u] *= (g[v] + (!u ? 0 : 1));
	}
	for (int i = 0; i < G[u].size(); i++) {
		pre[i] = (!i ? 1 : pre[i - 1]);
		if (G[u][i] == fa || flag[G[u][i]]) continue;
		pre[i] *= (g[G[u][i]] + (!u ? 0 : 1));
	}
	for (int i = G[u].size() - 1; i >= 0; i--) {
		suf[i] = (i == G[u].size() - 1 ? 1 : suf[i + 1]);
		if (G[u][i] == fa || flag[G[u][i]]) continue;
		suf[i] *= (g[G[u][i]] + (!u ? 0 : 1));
	}
	for (int i = 0; i < G[u].size(); i++) {
		int v = G[u][i];
		if (v == fa || flag[v]) continue;
		f[u] += (f[v] + g[v]) * (!i ? 1 : pre[i - 1]) * (i == G[u].size() - 1 ? 1 : suf[i + 1]);
	}
}

void solve() {
	cin >> n >> s;
	s--;
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;
		u--;
		v--;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs1(0, -1);
	if (!s) {
		dfs2(0, -1);
		cout << f[0] * 2 << "\n";
		return;
	}
	reverse(path.begin(), path.end());
	for (int i = 0; i < path.size(); i++) {
		flag[path[i]] = true;
	}
	dfs2(0, -1);
	dfs2(s, -1);
	for (int i = 1; i < path.size() - 1; i++) {
		dfs2(path[i], -1);
	}
	Mint ans = 0, calc = 1;
	vector<Mint> pre(int(path.size())), suf(int(path.size()));
	for (int i = 0; i < path.size(); i++) {
		calc *= g[path[i]];
		pre[i] = (!i ? 1 : pre[i - 1]) * g[path[i]];
	}
	for (int i = path.size() - 1; i >= 0; i--) {
		suf[i] = (i == path.size() - 1 ? 1 : suf[i + 1]) * g[path[i]];
	}
	for (int i = 0; i < path.size(); i++) {
		ans += f[path[i]] * (!i ? 1 : pre[i - 1]) * (i == path.size() - 1 ? 1 : suf[i + 1]);
	}
	Mint num = int(path.size()) - 1;
	cout << ans * 2 + num * calc << "\n";
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t = 1;
	// cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

::::

杂题

P4768 \(\color{purple}\text{紫}\)

学可持久化并查集。

注意到每一条边是可以离散化将范围降成 \(4\times 10^5\) 级别的,然后对于每次查询,二分一下便可以找到离散化后的淹没的值。

如果没有查询,我们可以使用并查集维护不包含淹没的边的连通块到 \(1\) 的最短距离,然后查询一下起点所在连通块的答案即可。对于多次的询问,可以使用可持久化并查集维护一下每种情况的状态,按海拔从高到低加入边,更新连通块的答案即可。

所以我们需要离散化、预处理最短路和可持久化并查集。感觉还是挺水的,只不过码量稍大。卧槽!!!卡常卡成史了!!!

P14761 \(\color{purple}\text{紫}\)

设此时的括号序列为 \(S\),则在加入一对括号之后考虑会对哪些括号造成影响。显然是中间的那些未匹配的左括号和右括号,所以答案就是这些括号的数量 \(\times 2\)。如何维护匹配的括号个数或未匹配的括号个数呢?考虑使用一个很经典的 \(+1-1\) trick,然后回顾合法括号序列的判断方法:

  • 前缀和均 \(\ge 0\)
  • 总和为 \(0\)

所以维护一个括号序列的前缀和和后缀和,判断一下这段区间的前缀和最小值的相反数和后缀和最大值,再 \(\times 2\) 就行了。由于要维护括号的插入,直接上 FHQ-Treap 即可。

这种垃圾结论能评紫?亏我还不相信自己看了一眼题解。

P14577 \(\color{green}\text{绿}\)

\(n\) 年之前的基础赛题。

有一个结论显然就是每一种字母只会有可能留下区间内的最后一个。(赛时就没观察出来,悬着的心彻底死了)然后考虑去维护这个东西。发现对于一种字母 \(c\)\(c_i\)\(c_{i+1}\) 中的区间只有两种状态(这是废话),这启发我们进行奇偶维护一段区间是否会被删除。然后便得到一个 \(O(q|S|^2)\) 的做法,即因为最后最多只有 \(26\) 个答案,所以直接枚举每一个答案可不可行,接着再枚举 \(26\) 个字母判断该答案是否被枚举到的字母覆盖。这个做法卡常可过,但不是正解。完了,发现自己不会待修,怎么办?傻逼了,操作看错了……

注意一个可能的答案 \(i\) 能不能存在当且仅当:

  • 存在字符奇数个字符 \(c\)\(i\) 的左边。
  • 存在字符 \(c\)\(i\) 的右边。

所以使用 \(26\) 位的二进制数来表达每一个位置的右边的 \(c\) 的出现次数的奇偶性,这个应该是可以前缀异或搞出来的,然后查询右边是否存在只需要用 \(\operatorname{ST}\) 表预处理 \(26\) 位二进制数然后用或来处理即可。搞区间最后出现的 \(26\) 个字母也就只需要对每一个位置预处理一个 \(26\) 种字母上一个出现的位置。

还是要多练位运算啊!

ACAM

P2322 \(\color{blue}\text{蓝}\)

想到的:看到这个数据范围就想到 acam 套一个 dp。然后你发现这个东西可以 bfs,但是不好记录哪些字符串已经存在,所以考虑状压一下,接着每次都跳 fail 指针把最长后缀都 push 进队列。为什么挂了一个点?如何判包含关系呢?暴力枚举一下就行了吧。

没想到的:为什么不用跳 fail 指针也可过呢?得好好理解一下。哦,原来是在 build 时候可以继承 fail 指针的状态。这下明白了。

P5231 \(\color{purple}\text{紫}\)

想到的:发现最长前缀没有好的处理方法,因此改成最长后缀,这样就或许可以用 ACAM 搞一搞了。这东西在 ACAM 上一次很难搞出来,是否需要通过拓扑优化一下呢?这个东西确实可以拓扑然后 dp 地取 \(\max\)。但是第一遍匹配的时候怎样计算答案。直接就是 \(u\)\(root\) 的距离了。这真的需要拓扑吗?直接跳 fail 指针然后标记一个 \(flag\) 使每一个节点只可能被跳到一次即可。哦,原来不用翻转字符串,只需要记录一下 \(id\),然后对于每次跳到的节点取 \(\max\) 就行了。这样 MLE+WA 了……主要问题应该还是 MLE。oh!,应该在跳 fail 之前就将 \(p\leftarrow son_p\)

没想到的:可以预处理哪些点可以成为前缀,然后再对于每一个询问匹配即可。

P2336 \(\color{purple}\text{紫}\)

做 fail 树。待续……

想到的:trie 树怎么建啊?先不管这些了,考虑到如果建出了 acam,那么直接搞 fail 树。但是有一个问题,怎么判重?即对于同时出现在名字和姓的人只能计算一次。考虑离线然后拆贡献,算一下一个人会被哪些询问点到。发现这样是好去重的,建两个 fail 树,跑两遍,记录一下第一遍被算过的字符串就好了。整理一下思路,刚刚那样子的时间复杂度不对。考虑对询问建立 acam,然后每次对于名字和姓进行查询,对于每一个点都开一个 bitset,然后按 fail 进行拓扑。oh!可以在 fail 树上做一个前缀和,这样就可以知道每一个名字。。。

一个奇妙的方法,在每一个姓和名之间添加一个 \(-1\),这样就可以直接建 ACAM。然后考虑如何 check。不会啊!怎样 check 包含关系啊!以询问建立 ACAM,然后。。。

看题解……题解写的什么几把。

P2292 \(\color{purple}\text{紫}\)

考虑一个朴素的 dp。对于每一次询问,设 \(f_i\) 表示前缀 \(1\sim i\) 是否满足题意。转移是简单的:

\[f_i \leftarrow \operatorname{OR}_{j\in [i-20, i - 1]}(f_j\land [\operatorname{substr}(j+1,i)\in D]) \]

显然过不去,需要优化。对模式串建出 ACAM 后,对于一个匹配到的点 \(u\),如果其在 fail 树上存在一个长度为 \(k\) 的祖先 \(v\) 且满足 \(v\) 为一个模式串的终止节点,则以 \(u\) 结尾、长度为 \(k\) 的后缀显然是一个模式串。

wow,状压妙妙题。注意到这样时间复杂度为爆掉,但是 \(|s|\) 很小,所以考虑能否通过状压加快速度。然后你可以先预处理一下一个节点 \(u\) 在 fail 树上的祖先中作为终止节点的长度,设其状压为 \(g_u\),转移为(此处在 fail 树上转移):

\[g_u\leftarrow g_{fa_u}+2^{len_u}\times [\operatorname{u\ is\ a\ end-node}] \]

然后在每一个匹配串匹配时记录一个状压值 \(x\),表示在 ACAM 上哪些位的 \(f\) 值为真,然后如果 \(x\operatorname{and} g_u\) 为真,就说明这个点显然也为真。时间复杂度在匹配时就变成线性了,好玩!

P2444 \(\color{purple}\text{紫}\)

不是很会做啊!

考虑 ACAM 的本质便是将 trie 树变成 trie 图。因此只需要在 trie 图上找是否存在不经过字符串的终止点的环即可。理解加深了许多。

P13352 \(\color{purple}\text{紫}\)

观察到枚举每一个文本串的后缀,然后判断有多少个与其匹配的前缀与在 ACAM 上跳 fail 是十分相像的,于是考虑以此求解该问题。

\(fail_u\) 表示 \(u\) 在 ACAM 上的 \(fail\) 指针指向的节点。则会与 \(u\) 产生贡献的节点便是:\(u,fail_u,fail_{fail_u},\cdots, 0\)。注意,因为 \(c^0=1\),所以 \(0\) 号节点也是要算进去的。因此,只需要知道有多少个字符串以 \(fail_u,fail_{fail_u},\cdots, 0\) 为前缀即可,这便等同于对如上的点查询其在 trie 中的子树和,同时,因为一个字符串只能与另一个字符串匹配一次,所以在查询完之后要将其子树的值均赋值为 \(0\)。这可以通过线段树维护 \(dfs\) 序完成。

具体的实现方法即为枚举每一个 trie 中的节点,然后暴力跳其所有的 fail,用线段树维护子树和。每枚举完一个节点就将线段树恢复为初始状态。因为保证了 \(\sum_{i=1}^{L}|s_i|\le 3\times 10^6\),所以这样的时间复杂度为 \(O(n\log n)\)

posted @ 2025-12-02 13:38  Tomwsc  阅读(14)  评论(0)    收藏  举报