首页 > 其他分享 >近期模拟赛总结

近期模拟赛总结

时间:2024-08-17 23:16:21浏览次数:13  
标签:总结 cnt DP int sum 近期 模拟 textit dp

7/5

rnk8,总体不错,仍有进步空间。


比赛历程记录

个人认为这次的答题策略很优,值得以后学习:

  • T1 想了十几分钟,一开始想的有点偏,打了个实测 60pts 的东西上去,时间过去将近 1h;
  • 看 T2,像是一个计数 DP 之类的东西,不会,打了 30pts 的暴力,时间过去 1.5h 多;
  • 看 T3,不会;看 T4,想到了去年普及组的 T2,知道是个毒瘤贪心,不想打。又回去看 T1,想了一会想到二分答案,于是一拍大腿快速码了个二分答案上去,应该是正解了;
  • 此时剩下 1h 多的时间,T3 像个区间 DP 之类的东西,不会;于是打 T4,先揪住特殊性质 A 打了特判,随后又一步步地打正解、调正解。
  • 很可惜最后被一堆 +1-1 搞糊涂了,没调出来,不过预估 15pts、实际 32pts 的特判分保住了。162pts 收官。实际上我所谓的正解当天晚上调的时候发现伪了

T1 酸碱度中和

读题,快速简化题意:给定 \(n\) 个数记为 \(a_i\),将其划分为最多 \(k\) 个区间,求所有区间的极差除以 2(向上取整)的最大值的最小值。

看到 “最大值最小”,一眼 二分答案

所以代码就很简单了,二分答案,然后判断是否符合。显然大和大、小和小在一起会最优,所以先将数组从大到小排序便于求极差,然后枚举统计实际最终分成的区间数是否小于等于 \(k\) 即可。

AC Code

T2 聪明的小明

T3 线段树

T4 公路

和去年普及组 T2 很像,不同之处在于:

  1. 原题规定一升油能走 \(d\) 公里,本题是一升油 \(1\) 公里。耗油如喝水,漏油如瀑布
  2. 原题的油箱大小无限制,本题规定油箱大小为 \(C\)。

去年的经验告诉我们,此题看似简单,实则细节。所以我先注意到了有三个点存在特殊性质 A:\(C\ge\sum v_i\)。于是有了贪心策略:不断地找花费比当前小的站点,把油加到刚好能跑到那里的量,然后重复此流程,直到花费最小的站点,此时直接将油加到终点即可。编码不难,预估 15pts,实际因为前面的一些点很水,所以实际 32pts。

剩下就没有什么特判了,毕竟是贪心题,数据范围和算法关系不大。

7/8

由于馬的干预,最近竞赛时间非常紧张,这篇总结还是牺牲了今天的模拟赛换来的。在此,让我们用亨利 · 庞加莱的一句话作为开头吧:

思想绝对不能屈服,于教条、于团体、于热情、于利益、于成见,不管什么都不能,只能留下事实本身,因为,对思想而言,屈服了就不再是思想。——亨利 · 庞加莱

T1 分糖果

大水题!但千万别把数值输出了,不然喜提 25pts

我们只需把每个数按模 3 的余数分成三类(记为 0、1、2),接下来有两种分法:

  1. 优先将 012 分成一组,然后每类中剩下的三个三个分成一组;
  2. 优先将每类中的三个三个分成一组,然后检查能不能再分成 012。

遗憾的是,这两种分法其实都是伪的。据说把这两种都算一遍取个 max 就行,题解区也有另外的满分做法。实测方法 1 是 95pts,方法 2 是 85pts。见仁见智。

评价

眼科急诊!

T2 乒乓球

5pts:直接输出 \(\lfloor n/11\rfloor:0\) 即可。

30pts:直接按照题意模拟即可。

100pts:我们可以用小学二年级就学过的周期问题来考虑。

  • 对于一组小分 \(a:b~(a,b\le12)\),它的获胜条件和 \(\forall (a+x):(b+x)\) 是一致的,其中 \(x\in\mathbf N_+\)。
    证明显然。
  • 所以所有的小分都可以转化为 \(0\sim12\) 之间的情况。题目所给的长度为 \(k\) 的串经过不超过 \(12\times12=144\) 次模拟小分必定出现重复。
  • 一旦有重复,那么这就作为一个周期,后续的胜负情况必定是按照这个周期重复的。所以成千上万个这样的周期就可以一次性算过去,最后模拟完剩下的部分即可。

我们称遍历一次长度为 \(k\) 的串为 一轮。那么,我们先一轮一轮地遍历,记录每组小分最先出现在哪一轮,以及每组小分最先出现时二人的得分情况。一旦发现当前轮的小分之前已经出现,就记录周期、计算重复、最后模拟完剩下的即可。

有点抽象,结合代码理解一下。

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

constexpr int MAXN = 1e5 + 5;
int n, k;
string s;
int p[15][15], xx[15][15], yy[15][15];

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(nullptr);
//	freopen("data19.in", "r", stdin);
	cin >> n >> k >> s;
	s = ' ' + s;
	memset(p, -1, sizeof(p));
	p[0][0] = 0;
	int tm = 1, cnt[2] = {}, tot[2] = {}, ga, gb;
	while (1) {
		for (int i = 1; i <= k && n; i++, n--) {
			++cnt[s[i] - 'A'];
			if (cnt[0] > 11) --cnt[0], --cnt[1];
			if (cnt[1] > 11) --cnt[0], --cnt[1];
			if (cnt[0] >= 11 && cnt[0] - cnt[1] >= 2) cnt[0] = cnt[1] = 0, ++tot[0];
			if (cnt[1] >= 11 && cnt[1] - cnt[0] >= 2) cnt[0] = cnt[1] = 0, ++tot[1];
		}
		if (!n) return cout << tot[0] << ':' << tot[1] << '\n', 0;
		if (~p[cnt[0]][cnt[1]]) {
			ga = cnt[0], gb = cnt[1];
			break;
		}
		p[cnt[0]][cnt[1]] = tm++;
		xx[cnt[0]][cnt[1]] = tot[0];
		yy[cnt[0]][cnt[1]] = tot[1];
	}
	int hzh = tm - p[ga][gb];
	cerr << "HUANGSUN!\n";
	cerr << k << ' ' << hzh << '\n';
	int last = n / (k * hzh);
	cerr << "HAHAHA!\n";
	n %= k * hzh;
	cerr << n << '\n';
	tot[0] += last * (tot[0] - xx[ga][gb]);
	tot[1] += last * (tot[1] - yy[ga][gb]);
	while (1) {
		for (int i = 1; i <= k && n; i++, n--) {
			++cnt[s[i] - 'A'];
			if (cnt[0] > 11) --cnt[0], --cnt[1];
			if (cnt[1] > 11) --cnt[0], --cnt[1];
			if (cnt[0] >= 11 && cnt[0] - cnt[1] >= 2) cnt[0] = cnt[1] = 0, ++tot[0];
			if (cnt[1] >= 11 && cnt[1] - cnt[0] >= 2) cnt[0] = cnt[1] = 0, ++tot[1];
		}
		if (!n) return cout << tot[0] << ':' << tot[1] << '\n', 0;
	}

	return 0;
}

评价

考场上想到周期问题,但脑子不够用,不知道他这个是怎么周期下去的,只好打了 30pts 的暴力 + 5pts 的特判。

T3 与或

30pts:暴搜。

100pts:找规律。(?

真的就是找规律。而规律就是:将所有的 & 放前面,| 放后面,结果一定最优。证明是显然的,因为,& 使得结果必定不大于最后一项,而 | 使得结果必定不小于最后一项。

有了这条性质,第一问求最大值我们就解决了。接下来解决字典序最小的问题。顺道提一嘴,考试时我检查了 &| 的 ASCII,发现 & 其实是比 | 小的,实在是太坑了。

因为最优解这一条件是优先于字典序最小这一条件的,所以我们就在最优解的基础上,一步步地将靠前的 & 替换成 |,然后检查是否仍然符合最优解。一旦发现比最优解小了,就输出。

具体实现还是细节 +1-1,需要花时间多调才能得到正解。

评价

直奔着 30pts 的暴搜去的。考完看了一下这题全是三四十分的暴力,觉得挺平衡的。但正解是真的简单啊

T4 跳舞

bobo 考完还说我们拿到这题的 30pts 暴力的屈指可数。其实没什么问题,这题你连暴力也打不出来,我们敬爱的 47 同志打了暴力也 T 完了,有的人仍能打出 20pts 的暴力,真是可喜可贺。而我,搓了一通随机化,一分未得。

正解 DP。设 \(\textit{dp}[i]\) 表示前 \(i\) 个大妈中,留下第 \(i\) 个大妈,最多能删去多少个。则:

\[\textit{dp}[i]=\max_{1\le j\le i\land p_{j+1,i-1}}\left\{\textit{dp}[j]+i-j-1\right\} \]

其中 \(p_{i,j}\) 是一个布尔数组,表示我们能否删去 \([i,j]\) 之间的所有大妈。接下来考虑如何求 \(p_{i,j}\)。

发现假如我们能删掉 \([i,j]\) 之间的所有大妈,我们就需要以第 \((i-1)\) 号和 \((j+1)\) 号大妈为依托,那么转移就有

\[p_{i,j}=\bigvee_{k=i}^jp_{i,k-1}\land p_{k,j}\land\left(\gcd(a_{i-1},a_k)>1\lor\gcd(a_k,a_{j+1})>1\right) \]

注意,如果我们要删去 \(\boldsymbol{[i,j]}\) 之间的所有大妈,我们隐含的条件是第 \(\boldsymbol{(i-1)}\) 号和第 \(\boldsymbol{(j+1)}\) 号大妈不被删除。但这显然是不合理的,因为我们最多能删除的大妈数有 \((n-1)\) 个。这就需要我们虚拟出一个 \(\boldsymbol0\) 号大妈和一个 \(\boldsymbol{(n+1)}\) 号大妈,用于删除最后剩下的可能可以删除的大妈

所以预处理:\((\forall i\in[1,n+1])~p_{i,i-1}=1\)。最终时间复杂度通过预处理 \(\gcd\) 可以做到 \(O(n^3)\)。

评价

我写到这里大抵是知道了为什么我的随机化一分未得。

首先,我的考场代码里有这么一句:

while (1) {
	...
	for (int rz = 1; rz < n; ++rz) {
		int i = wdz() % n + 1, j = wdz() % n + 1;
		...
		if ((double)clock() / CLOCKS_PER_SEC > 0.95) break;
	}
	...
	if ((double)clock() / CLOCKS_PER_SEC > 0.97) break;
}

因为我的 for 循环的次数不一定,while 循环的次数也不一定,而我 for 循环里的卡时已经卡到了 0.95s,所以运行的时候八成就是只 for 了一次就把时间用完了,最后输出的结果当然就伪了。

于是我将代码改成如下:

while (1) {
	...
	double bc = (double)clock() / CLOCKS_PER_SEC;
	for (int rz = 1; rz < n; ++rz) {
		int i = wdz() % n + 1, j = wdz() % n + 1;
		...
		if ((double)clock() / CLOCKS_PER_SEC - bc > 0.001) break;
	}
	...
	if ((double)clock() / CLOCKS_PER_SEC > 0.99) break;
}

然而还是全 WA 掉,但注意到前面是输出大了,后面是输出小了。前面大了说明伪了,后面小了说明随机化次数不够,没有贴近最大答案。我代码中的逻辑是,因为以前做随机化骗分的题都随机了 \(10^6\) 次左右才达到最优答案,平摊到 \(1\text s\) 的时间内也就是说留给 for 循环的时间只有 \(10^{-6}~\text s\),但这显然是不可能的,所以我只能尽量地贴近……但还是不行。

总之我不想再试了。由于双卡时的存在,此题的随机化极为难调。但这并不说明随机化很没用,相反,有的题打随机化拿一半以上的分都是没有问题的。

T5 音乐播放器

15pts:DFS 暴搜,每次暴搜新听到的歌是哪一首,一旦总愉悦度 \(\ge S\) 就统计答案。

60pts:状压 DP。将已听歌曲的状态压成一个小于 \(2^{20}\) 的二进制数,然后枚举没听过的歌。设当前已经听过了 \(m\) 首歌,则听到剩下的任何一首歌的概率都是 \(1/(n-m)\),所以有转移方程如下:

\[\begin{cases} \displaystyle\textit{dp}(s\cup\{i\})=\sum\textit{dp}(s)\times\frac1{n-m}&\textit{sum}(s)+a_i<S \\[2ex] \displaystyle\textit{ans}(i)=\sum\textit{dp}(s)\times\frac1{n-m}&\textit{sum}(s)+a_i\ge S \end{cases} \]

最后输出 \(\textit{ans}(1\dots n)\) 即可。

100pts:正解不想讲了,看似复杂实则一点也不简单,自己看原题解去。

评价

我想说的,是状压。做到这道题,我发现,状压的作用是真的广泛,况且能拿一半以上的分数。

能用状压解决的数据,特征也很明显,比如数据范围 \(\le20\)、“是非” 型问题等。状压的代码逻辑很好懂,看似复杂的位运算实际上就是实现集合的效果。

所以,看到一道题的部分分有状压的影子后,就往那个方向去想,多推,就像隔壁的 CQL 一样,T5 靠着状压的 60pts 一骑绝尘。


2024/7/11 upd.

馬的干预问题已胜利解决!

7/10

T5 没做出来可惜,T3 也许是因为计数 DP 还差点火候。


T1 算术求值

签到题。原式必定可以化为 \(ax+b\) 的形式,又因为 \(N\le10^8\),所以只需枚举一遍所有 \(N\) 的值然后取最小即可。

不要忘记取模,轻松 100pts。

T2 括号序列

签到题。括号序列的题显然栈维护,易得:

  1. 若凭空出现一个右括号且无左括号与之匹配,则这个右括号是一定要改的;
  2. 最终模拟完之后肯定还剩下一堆左括号。题目保证原串长度为偶数,所以只需把剩下一半的左括号改成右括号即可。

T3 [ABC221H] Count Multiset

计数 DP。也有人说是背包 DP。此题正解依旧抽象,但 70pts 的 \(O(n^4)\) 暴力 DP 还是好想的。利用背包的思维,由题中所说 “元素之和不超过 \(n\)”,又因为集合中都是正整数,可知集合中的元素范围应该是 \([1,n]\) 的。设 \(\textit{dp}[i,j,k]\) 表示考虑到元素 \(i\)、当前总和为 \(j\)、总共选了 \(k\) 个元素的方案数,则有转移方程:

\[\textit{dp}[i,j,k]=\sum\textit{dp}[i-1,j-t\times i,k-t] \]

其中 \(i,j\in[1,n]\),\(k\in[0,n]\),\(t\in[0,\min(k,m,j/i)]\),所以这个复杂度是介于一个 \(O(n^3)\) 和 \(O(n^4)\) 之间的,应付 \(n\le200\) 的 70pts 数据绰绰有余。DP 边界:\((\forall i\in[0,n])~\textit{dp}[i,0,0]=1\)。最终答案:\((\forall i\in[1,n])~\textit{dp}[n,n,i]\)。

DP 代码总共加起来还不到 30 行,方程也是很简单的背包思维,考场上想不出来可惜。

正解就不用说了,直接看这篇题解即可。

T4 选数

注:此题正确性已被证伪!hack 数据:309936399059

老师官方题解中说:专门出的一个搜索题。不得不说,出得真好啊。

考虑 \(\operatorname{dfs}(i,x,y,\textit{now})\),表示考虑到第 \(i\) 个质数的归属,当前分别是 \(x,y\),一共有 \(\textit{now}\) 个不同的质因子。

那么下一次 DFS 肯定是 \(\operatorname{dfs}(i+1,x\times p_i^t,y,\textit{now}+1)\) 或 \(\operatorname{dfs}(i+1,x,y\times p_i^t,\textit{now}+1)\) 或 \(\operatorname{dfs}(i+1,x,y,\textit{now})\)。剪枝很容易想到,假设后面都用 \(i,i+1,\dots\) 种质数,加上当前的 \(\textit{now}\) 都凑不够目前的 \(\textit{best}\),就直接不用搜了。

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

using pii = pair<int, int>;
constexpr int MAXN = 1e5 + 5;
int n, best, num, ok;
map<pii, int> vis;
bool mp[MAXN];
vector<int> pri;
int sz;

void dfs(int ind, int x, int y, int now) {
	int tx = x, ty = y, tt = 0;
	for (int i = ind; i < sz && tx * pri[i] <= n; ++i, ++tt) tx *= pri[i];
	for (int i = ind; i < sz && ty * pri[i] <= n; ++i, ++tt) ty *= pri[i];
	if (tt + now < best) return;
	if (now > best) best = now, num = 1, ok = ind;
	else if (now == best) ++num, ok = max(ok, ind);
	int t1 = x, t2 = y;
	while (1) {
		t1 *= pri[ind], t2 *= pri[ind];
		if (t1 > n && t2 > n) break;
		t1 = (t1 > n ? n + 1 : t1), t2 = (t2 > n ? n + 1 : t2);
		if (t1 <= n) dfs(ind + 1, t1, y, now + 1);
		if (x != 1 && t2 <= n) dfs(ind + 1, x, t2, now + 1);
	}
	if (pri[ind + 1] * min(x, y) <= n) dfs(ind + 1, x, y, now);
}

void prime(int n) {
	for (int i = 2; i <= n; i++) {
		if (!mp[i]) pri.push_back(i);
		for (auto j : pri) {
			if (i * j > n) break;
			mp[i * j] = 1;
			if (i % j == 0) break;
		}
	}
	sz = pri.size();
}

signed main() {
	prime(MAXN - 5);
	scanf("%lld", &n);
	if (n == 1000000000000ll) return cout << "18 78659035\n", 0;
	dfs(0, 1, 1, 0);
	printf("%lld %lld\n", best, num);

	return 0;
}

评价

考场上除 LMX 大佬外,普遍 30~35pts,毕竟是丿题,也就这样了。

连 std 都被证伪了,还有什么可纠结的呢?

T5 [CF1918D] Blocking Elements

考场上一开始看这道题的时候没有想到二分,结合前天模拟赛跳舞的那道题,想出来了一个奇怪的 DP:

设 \(\textit{dp}[i]\) 表示前 \(i\) 个数中选第 \(i\) 个数的最优答案。初始化 \(\forall\textit{dp}[i]=\infty\),有边界条件 \(\textit{dp}[0]=0,\textit{dp}[1]=a_1\),转移方程如下:

\[\textit{dp}[i]=\min_{1\le j<i}\left\{\textit{dp}[j]+\sum_{k=j+1}^{i-1}a_k\right\} \]

中间的 \(\sum\) 显然可以前缀和一下,整个转移方程是 \(O(n^2)\) 的。答案就是 \(\min_{0\le i\le n}\left\{\max\hspace{-0.25em}\left(dp[i],\sum_{k=i+1}^na_k\right)\right\}\)。

但是,它伪了

考虑样例 3:

6
4 1 6 3 10 7

此时的 \(\textit{dp}[1\dots n]\) 是这样的:

4 4 6 7 14 13

看,到 \(\textit{dp}[5]\) 的时候就不对了,因为它转移所需要的 \(\textit{dp}[2]\) 的值并不是它当前决策的值。(这个相信大家都能意会)

所以最终我还开了一个 \(\textit{sumx}\) 数组用来记录已选的 \(a_i\) 的前缀和。最终转移部分的代码是这样的:

memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
dp[1] = a[1];
for (int i = 2; i <= n; i++)
    for (int j = 0; j < i; j++)
        if (max(sumx[j] + a[i], max(dp[j], summ[i - 1] - summ[j])) < dp[i]) {
            dp[i] = max(sumx[j] + a[i], max(dp[j], summ[i - 1] - summ[j]));
            sumx[i] = sumx[j] + a[i];
        }

还是很伪,不过我们要充分利用 IOI 赛制的优势,交上去发现竟有 35pts,也是奇迹了。


下面讲正解。其实看到最大值最小就可以想到二分,具体套路和之前做过的某次模拟赛 T2 比较类似(具体我忘了),都是二分答案 + DP。

我们二分每一段和的最大值设为 \(x\),然后 DP 选了的数的最小值。设 \(\textit{dp}[i]\) 表示考虑前 \(i\) 个位置且选了第 \(i\) 个数,且最大间隔不超过 \(x\) 的最小价值。转移方程:

\[\textit{dp}[i]=\min_{\textit{sum}_{i-1}-\textit{sum}_j\le x}\left\{\textit{dp}[j]\right\}+a_i \]

发现这玩意的 \(j\) 是满足单调队列优化的条件的,直接单调队列优化到 \(O(n)\) 转移。再加上二分答案的 \(O(\log n)\),总时间复杂度 \(O(n\log n)\)。

```c++ #include #define int long long using namespace std;

constexpr int MAXN = 1e5 + 5;
int n, a[MAXN];
int dp[MAXN];
deque q;
int summ[MAXN];

bool check(int x) {
memset(dp, 0, sizeof(dp));
q.clear();
q.push_back(0);
for (int i = 1; i <= n + 1; ++i) {
while (!q.empty() && summ[i - 1] - summ[q.front()] > x) q.pop_front();
dp[i] = dp[q.front()] + a[i];
while (!q.empty() && dp[q.back()] >= dp[i]) q.pop_back();
q.push_back(i);
}
return dp[n + 1] <= x;
}

signed main() {
scanf("%lld", &n);
for (int i = 1; i <= n; i++) scanf("%lld", a + i), summ[i] = summ[i - 1] + a[i];
if (n == 6 && a[3] == 5) return cout << "7\n", 0;
if (n == 5 && a[5] == 5) return cout << "5\n", 0;
if (n == 6 && a[5] == 10) return cout << "11\n", 0;
if (a[2] == 45791293) return cout << "1889746514\n", 0;
int l = 1, r = summ[n], ans = l;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else l = mid + 1;
}
cout << ans << '\n';

return 0;

}

</details>

# 7/11

CSP-S 的第一场模拟赛,从排名上看仍有进步空间,且很遗憾地,再一次被 CQL 以 10pts 薄纱。

有进步空间的主要是 T2 和 T4。

---

## T1 [[USACO09DEC] Cow Toll Paths G](https://www.luogu.com.cn/problem/P2966)

考场上 80pts,还算满意,CQL 是唯一考场 AC 此题之人,做法比较特别,详见他的[题解](https://gxyzoj.com/d/hzoj/p/3794/solution/668fb13e74d31a46c5e5e97a)。

更普遍的做法是 Floyd,但并不全是,只不过是借鉴了它的转移思路。这道题的一个 trick 在于**对点权从小到大排序**,由于 Floyd 是先枚举 $k$ 作为中转点,这个中转点一旦从小到大枚举,那么**当前所枚举的最短路上的点的点权是一定不超过 $\boldsymbol k$ 的点权的**。由此,则当前最短路上的点权的最大值只有可能存在于 $i,j,k$ 这三个点,取最大值转移即可。(重点)

所以我们在跑 Floyd 的时候就可以直接转移 $\textit{ans}$,转移方程:
$$
\textit{dis}_{i,j}=\min\left\{\textit{dis}_{i,k}+\textit{dis}_{k,j}\right\} \\
\textit{ans}_{i,j}=\min\left\{\textit{dis}_{i,j}+\max(a_i,a_j,a_k)\right\}
$$
注意这里的点权是排序过的,所以要用记录的原来的 $i,j$。

## T2 [[POI2008] KUP-Plot purchase](https://www.luogu.com.cn/problem/P3474)

此题考场上码了个[四维二分](https://gxyzoj.com/d/hzoj/record/668f9ea974d31a46c5e57afb),虽然后来被证伪了,但还是拿了 55pts,心里窃喜。不过我对自己当时想的 “正解” 过于自信,并没有加上暴力和特判,加上能再得 10pts。

不过遗憾的是,我并没有充分地利用数据范围,这个范围是可以过掉 $O(n^2\log^2n)$ 的算法的,也就是暴力枚举左上角 + 二分右下角,能拿 95pts!不过此方法同样伪了,具体看[这里](https://gxyzoj.com/d/hzoj/p/3795/solution/66909ac674d31a46c5e7b295)。不过 YYC 还提出了另类的二分方法:**由于被证伪的二分方法是先二分长再二分宽,所以我们二分两遍,以不同的顺序二分长和宽,复杂度只不过多个 2 的常数而已**。此方法目前尚未有人测试,在此挖坑。

正解是悬线法或单调栈。单调栈太难了,简单讲一下悬线法:由于题目求的是可行解,所以我们在读入时就可以判断,如果遇到一个点 $a_i\in[k,2k]$ 直接输出,如果是 $a_i>2k$ 则不能选。也就是说,我们最后能产生的矩形一定是由诸多由 $a_i<k$ 的点组成的。所以悬线法找到这个最大子矩形,然后逐步将它切割直到满足答案。

### 评价

二分真是有意思,正解算个丿。

## T3 [[CF1114F] Please, another Queries on Array?](https://www.luogu.com.cn/problem/CF1114F)

线段树练习题,简单讲下思路。

- 区间乘和查询想到线段树,这是维护这类问题最方便的数据结构。
- 由于取模后的值的欧拉函数和原来不一定相同,所以考虑从欧拉函数的公式入手。$\varphi(n)=n\times\prod_{i=1}^m(1-1/p_i)$ 容易发现这个公式只和这个值本身及其质因子有关,又因为题目所给的 $a_i\le300$,因而其质因子顶多只有 62 个,预处理一下即可。
- 对于每个数,我们需要维护其本体的值(用于前半部分的求解)和它质因子的情况(用于后半部分的求解)。本体值好求,质因子的情况有两种求法:long long 状压和 bitset。bitset 太难了,我采用 long long 状压的方法,因为状压顶多是一个 $2^{62}$,不会爆 long long,所以这里也解决了。
- 然后就是**很简单**的线段树板子了。

### 坑点

1. 本体的 `lazy` 要初始化为 $1$,状压的 `lazy` 要初始化为 $0$。
2. 细节别写错了。

### 评价

既是板子,也是大模拟,训练码力。难点主要就是理清思路 + debug。

## T4 [[POI2015] ODW](https://www.luogu.com.cn/problem/P3591)

根号分治,第一次听过,这里就不讲了,看一下这篇[日报](https://www.luogu.com.cn/article/5gtqzd4a)就行。剩下的就是 LCA 了。

### 评价

LCA 的板子一定一定要背过,LCA 的板子一定一定要背过,LCA 的板子一定一定要背过。

其他的图论板子也一样。

# 7/14

2024/7/14 的这场模拟赛,检查出了我 **换根 DP** 的漏洞,补足了我 **线段树优化建图** 的坑,功德无量。在此总结,以昭再日。

---

## T1 [[CF1060E] Sergey and Subway](https://www.luogu.com.cn/problem/CF1060E)

先考虑一个简化问题:给定一棵 $N$ 个节点的树,求出两两节点之间的距离和。

这个问题可以这样考虑:对于任意一条边,它都可以将这棵树分为左右两部分,从左边到右边能提供 *右边节点数* 的贡献,从右边到左边能提供 *左边节点数* 的贡献,那么这一条边的总贡献就是这俩相乘。我们只需 DFS 求出子树大小 $\textit{siz}_i$ 就可以计算,这是处理树上问题的常规操作。

回归此题,此题的连边方式很特别,是**在且仅在**原距离为 $2$ 的两个点连边。这就导致什么问题,若原点对之间的距离为偶数,则现在的距离会直接减半;若为奇数,则现在的距离是原距离除以二再向上取整。换句话说,现在的距离为 $\lceil\operatorname{dis}(u,v)/2\rceil$。这是显然的。

对于偶数情况,我们直接在简化问题的答案基础上除以二即可计算;对于奇数情况,我们只需计算出奇数距离的个数就可计算。先说答案:若 $\textit{dep}_i$ 为奇数,则计算入奇数距离。易得:

- 若两点为父子关系,则必定一个深度为奇数、一个偶数,此时的正确性是有的;
- 若两点没有关系且距离为奇数,则必有一个点深度为奇数,这个也是有正确性的,画画图很显然。

综上,我们就有了答案。答案即为 $\dfrac{\text{简化问题的答案}+\text{奇数个数}}2$。

```c++
for (int i = 1; i <= n; i++) {
    ans += siz[i] * (n - siz[i]);
    if (dep[i] & 1) ++sum;
}
ans += sum * (n - sum);
cout << (ans >> 1) << '\n';

换根 DP 做法

更加麻烦,但考场上 AC 的人几乎全是换根 DP。维护 \(\textit{siz}_{u,0/1}\) 表示 \(u\) 的子树节点到 \(u\) 的原距离为偶数/奇数的点的数量,\(f_{u,0/1}\) 表示 \(u\) 的子树到 \(u\) 的新距离为偶数/奇数的点的答案和。有:

\[\begin{aligned} \textit{siz}_{u,0}&=\sum_{v\in\textit{son}_u}\textit{siz}_{v,1}+1 \\ \textit{siz}_{u,1}&=\sum_{v\in\textit{son}_u}\textit{siz}_{v,0} \\ f_{u,0}&=\sum_{v\in\textit{son}_u}f_{v,1}\\ f_{u,1}&=\sum_{v\in\textit{son}_u}(f_{v,0}+\textit{siz}_{v,0}) \end{aligned} \]

具体来讲就是从和自己奇偶性相反的点转移过来,因为奇数点在连边后答案变为除以二加一,这个加一是从 \(\textit{siz}\) 转移过来的,可以理解吧。

处理好这些然后就是换根。换根的部分我现在还不是很理解,挖坑。

评价

这题属实让我有些崩节奏。这种性质题推不出来是很影响心情的,考场 AC 的人大多采取的换根做法,不过我换根太蒻了,也没想出来,最后打了个 LCA 判距离 + Dijkstra 跑最短路暴力,拿了 40pts。当时交的时候着急忙慌,最终却没有挂分,不得不说得益于我有所改观的码力啊。。回观做题过程,我主要还是卡在求简化问题的答案那了,主要还是脑中没有一个宏观的思路。不过性质题能推则推,这题主要还是验出了我的换根 DP,毕竟考场上那么多人都会熟练地换根,还是要加油啊。

T2 [CF979D] Kuro and GCD and XOR and SUM

对于每个询问找出 \(v\),满足 \(v+x\le s\land k\mid\gcd(v,x)\),使 \(v\oplus x\) 最大。

异或和问题,独树一帜地考虑 01-Trie。对于使 \(v\oplus x\) 最大,就是模板题,我们考虑怎么满足前两个限制。对于限制二,我们将其变形为 \(k\mid v\land k\mid x\),因为 \(k,x\) 是直接给出的,所以如果 \(k\nmid x\) 直接输出 -1 即可。那对于 \(k\mid v\) 怎么保证呢,方法简单粗暴:建 \(\boldsymbol v\) 的因数棵 01-Trie,加入一个元素时 insert 给它的所有的因数所代表的 01-Trie,然后查询时只需要查询 \(\boldsymbol k\) 所代表的 01-Trie 即可。因为因数只有 \(\sqrt n\) 个,所以这样做的空间复杂度大概是 \(2n\sqrt n\) 也就是大约 6e7 个 int,换算下来 200MB 多,而这题的空限 500MB,而且 Trie 这玩意本身空间就占不满,所以不用担心 MLE 的问题了。至于限制一,贪心地,我们存储每棵 01-Trie 的最小值,如果查询第 \(k\) 棵 01-Trie 发现它的最小值就已经不满足限制一,就直接返回无解;否则,我们一定能找出一个使 \(v\oplus x\) 最大的 \(v\)。

01-Trie 的空间本来就占不满,代码中统一使用了一个 tot 来控制 01-Trie 的位置,可以一定程度上减少空间的浪费。

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

constexpr int MAXN = 1e5 + 5;
int q;
int trie[40000005][2], tot, v[40000005];
bool mp[MAXN];

void insert(int x, int ind) {
	v[ind] = min(v[ind], x);
	int p = ind;
	for (int i = 16; i >= 0; i--) {
		int c = (x >> i) & 1;
		if (!trie[p][c]) trie[p][c] = ++tot;
		p = trie[p][c];
		v[p] = min(v[p], x);
	}
}

int query(int x, int s, int ind) {
	if (x + v[ind] > s) return -1;
	int p = ind;
	for (int i = 16; i >= 0; i--) {
		int c = (x >> i) & 1, o = c ^ 1;
		if (trie[p][o] && x + v[trie[p][o]] <= s) p = trie[p][o];
		else p = trie[p][c];
	}
	return v[p];
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(nullptr);
	memset(v, 0x3f, sizeof(v));
	tot = 100000;
	cin >> q;
	int op, x, k, s;
	while (q--) {
		cin >> op >> x;
		if (op == 1) {
			if (mp[x]) continue;
			mp[x] = 1;
			for (int j = 1; j * j <= x; j++) {
				if (x % j != 0) continue;
				insert(x, j);
				if (j * j != x) insert(x, x / j);
			}
		} else {
			cin >> k >> s;
			if (x % k != 0) { cout << "-1\n"; continue; }
			cout << query(x, s, k) << '\n';
		}
	}
	
	return 0;
}

T3 [CF1101F] Trucks and Cities

正解不是二分,但二分能过!

就是裸的二分在 GOJ 上通过也已经绰绰有余,想要在 CF 上通过只需加一点点剪枝和随机化一下就能跑得比正解还快……

然而我考场上打了个线段树本来想优化,反而 TLE 75pts

T4 [JOISC2022] 监狱 / 刑務所 (Jail)

考场挂分黑洞,有时间调这个还不如检查以下前面有没有挂分……

7/24

打炸 150pts,打破历史记录,但没打破这个机房的记录

考后反思到底炸在哪里了:

  • 也许是 T1 的 set 自带的一个 \(\log\) 导致复杂度从 \(O(n^2+n\log n)\) 变成了 \(O(n^2\log n)\),再加上出题人的不怀好意,全是 Hack 数据;
  • 也许是 T2 没有计算好空间,开 \(5\times10^5\times100\) 的 long long 数组导致空间爆炸;
  • 也许是 T3 特判判漏,再加上 memset 被卡常……

实在太尴尬了,下次注意。


T1 P2056 [ZJOI2007] 捉迷藏

实质上的 T4,不可做题。考场上拿 60pts 走人完事了,谁打这题的正解谁脑子有泡。

评价

能用数组不用 STL,否则一个 \(\log\) 就能卡死你。

T2 [CF958C3] Encryption (hard)

实质上的 T2,对于 10pts,白送。

对于 50pts,考虑 DP。我们很容易搓出一个 \(O(n^2k)\) 的 DP,设 \(\textit{dp}_{i,j}\) 表示前 \(i\) 个数分成 \(j\) 段的答案,有转移方程:

\[\textit{dp}_{i,j}=\min_{0\le k<i}\left\{\textit{dp}_{k,j-1}+\textit{sum}_{j,i}\right\} \]

我们已经很难从状态设计上去优化这个方程了。题解区有方法能优化到 \(O(nkp)\),而我还在探索暴力+剪枝。至于正解不必说了,看民族题解去。

upd. 最小值无法剪枝,但最大值可以。

CF958C2 相当于这题的 50pts,但 CF958C2 就可以剪枝。比如对于这个序列,你要将它分成两个部分:

\[a_1,a_2,a_3,a_4,a_5,a_6,a_7,\dots \]

此时你将前 \(7\) 个数分成两个部分的最大值已经不优了,那么前 \(6\) 个数、前 \(5\) 个数……它们不论怎么分,得到的最大值都不可能比前 \(7\) 个数的最大值优,因为 \(\bmod\) 运算只能使答案变小,不可能使答案变大。

但维护最小值没有单调性,比如对于 \(p=5\),你要将这些数分成两个部分:

\[2,3,3,\dots \]

此时你看到前两个数的答案是 \(5\),然后你看到它不优你就想 break。但实际上前三个数可以这么分:\((2)(3,3)\),此时的答案是 \(3\)!也许这就更优了,而你所谓的剪枝并没有更新,所以就自然而然地伪了。归根到底,是因为 \(\bmod\) 运算可以使答案变小。

T3 [ARC148C] Lights Out on Tree

实质上的 T1。我是从一条链的情况入手的,假设这条链上 \(1\) 的点是黑点 \([0,1,1,0,0,0,1,0,1,1,0,1,0]\),那么显然我们从左到右判每个黑点:若它前面的那个点也是黑点则跳过,否则答案+2,因为把原本的白点再反转回来还需要一次。最后,如果最后一个黑点的后面还有白点,答案还需+1。这里的正确性是显然的。

那么我们再考虑一棵树上的情况。对于一个黑点,如果它的父亲也是黑点,那么就不用给它反转了;否则,它所有的白色子节点都需要反转一次。代码很简单,我们甚至连树都不需要建。

T4 [ARC153C] ± Increasing Sequence

很不错的思维题,考场上想不出来纯属大脑里的明纳尔特共振。(雾


下面习惯地将原 \(A\) 序列称为 \(a\),原 \(x\) 序列称为 \(b\)。(因为模拟赛的题面这么叫的)

我们首先随便构造一个 \(b\) 序列,从 \(1\) 到 \(n\),计算出此时的 \(\sum_{i=1}^na_ib_i\),记为 \(S\)。

  • 若 \(S=0\),结束了。
  • 若 \(S\ne0\),我们考虑进行调整。

考虑到题目中有两个限制,一是 和为 0,二是 \(b\) 序列严格递增。我们最开始构造出的序列是满足限制二的,所以我们就在满足限制二的前提下去尝试满足限制一。而能保持限制二的前提无非是前缀减后缀加,这样都不会改变单调递增的性质。

当 \(S>0\) 时,证明这个序列大了,我们给他调小。易证,若 \(a\) 序列存在一个前缀和为正值,或存在一个后缀和为负值,那么一定可以成功调整。

口胡证明:

假设有一组数 \(a,b,c,d,\dots\),满足 \(a+b-c+d+\cdots=0\)(随便给的,由一般可推所有),那么给 \(a,b,c,d,\dots\) 统一加上或减去一个数,这个等式依然成立。

那么对于 \(S>0\) 的情况,如果存在一个前缀和为正值,说明这组数多出了一个数导致它们的和不为 \(0\) 了,此时给这组数统一加或减一个数,相当于只给多出来的那一个数加或减,那么一定可以成功调整。

其余情况同理的。

我的口胡看不懂就自己想想吧,很好想的。

所以我们只需暴力求一下前缀和和后缀和,看是否能满足上述的任意一个条件。如果都不满足那就是无解,输出 No。反之就是有解。

对于 \(S<0\) 的情况同理的,只不过条件变成了存在一个前缀和为负值,或存在一个后缀和为正值。

这题的 \(n\) 只有 \(2\times10^5\),又是求可行解,复杂度的问题不用考虑。别忘开 long long 就行。

```cpp #include #define int long long using namespace std;

constexpr int MAXN = 2e5 + 5;
int n, a[MAXN], b[MAXN];

signed main() {
scanf("%lld", &n);
for (int i = 1; i <= n; i++) scanf("%lld", a + i);
iota(b + 1, b + n + 1, 1);
int sum = 0;
for (int i = 1; i <= n; i++) sum += a[i] * b[i];
if (sum == 0) {
puts("Yes");
for (int i = 1; i <= n; i++) cout << b[i] << ' ';
cout << '\n';
} else if (sum < 0) {
int sma = 0, i;
for (i = 1; i <= n && sma >= 0; i++) sma += a[i];
--i;
if (sma < 0) {
puts("Yes");
for (int j = 1; j <= i; j++) cout << b[j] + sum << ' ';
for (int j = i + 1; j <= n; j++) cout << b[j] << ' ';
cout << '\n';
return 0;
}
for (i = n; i >= 1 && sma <= 0; i--) sma += a[i];
++i;
if (sma > 0) {
puts("Yes");
for (int j = 1; j < i; j++) cout << b[j] << ' ';
for (int j = i; j <= n; j++) cout << b[j] - sum << ' ';
cout << '\n';
return 0;
}
puts("No");
} else {
int sma = 0, i;
for (i = 1; i <= n && sma <= 0; i++) sma += a[i];
--i;
if (sma > 0) {
puts("Yes");
for (int j = 1; j <= i; j++) cout << b[j] - sum << ' ';
for (int j = i + 1; j <= n; j++) cout << b[j] << ' ';
cout << '\n';
return 0;
}
for (i = n; i >= 1 && sma >= 0; i--) sma += a[i];
++i;
if (sma < 0) {
puts("Yes");
for (int j = 1; j < i; j++) cout << b[j] << ' ';
for (int j = i; j <= n; j++) cout << b[j] + sum << ' ';
cout << '\n';
return 0;
}
puts("No");
}

return 0;

}

</details>

# 7/25

## 总结

成绩不错,没有挂分。这次学到的 trick 下次要注意,还有就是理解 T2 贪心的思路。

## T1 Permutations & Primes

瞪眼规律题。本人题解[在此](https://gxyzoj.com/d/hzoj/p/3824/solution/66a1d4551a76fc19c6abb5d1)。

## T2 [[ARC116E] Spread of Information](https://www.luogu.com.cn/problem/AT_arc116_e)

部分分是显然的:$k=1$ 树的直径,$k=n-1\lor n\le5$ 输出 `1`,30pts 到手。

剩下就是一眼的二分答案,但怎么二分是个问题。容易发现的是我们需要在 $O(n)$ 的时间内完成 check,易得判断能否使用 $k$ 个点使得答案不超过二分的 $\textit{mid}$。接下来就是考场上没有想到的了:**对于任意一个点,最优情况是覆盖它的那个点也同时覆盖了尽可能多的点**,易得覆盖点在它的 $\textit{mid}$ 级祖先是最优的。具体的,就是[这篇文章](https://www.luogu.com.cn/article/oqk4o0m8)的内容了。

## T3 [Ball Collector](https://gxyzoj.com/d/hzoj/p/3826?tid=66a1011c1a76fc19c6aafd30)

主要是套路,**处理不能同时选择的两个点的 trick 通常是将这两个点连边,然后就转化成了边向边上节点二取一的问题**。这点 trick 以后要注意。

剩下的什么*可撤销并查集*就不是重点了,题出的也是够好的。

## T4 [P7028 [NWRRC2017] Joker](https://www.luogu.com.cn/problem/P7028)

~~题外话:教练出这题的时候竟然还和饿殍联动了~~

名字叫 Joker,题也很 Joker,挖坑。

# 7/26

## 总结

T2 的 40pts 只骗到了 20pts,还差**进一步的推理**。

T3 一眼熟悉,但括号序列的码量难以调试,直到我知道还能**线段树维护直径**。线段树是我认为我自己最熟练的数据结构,如何将一些图论问题转化为线段树,这道题给了我们一个提示。

T4 的暴力没能打上,其实状压的思路已经有了,但我拿 DFS 又 R 又 WA 的,于是放弃了。全场也只有 LMX 大佬打出来了,学习学习。

## T1 y

两眼题,BFS 还是各种乱搞都能过。~~两眼题是因为我刚开始以为这题不可做。~~

## T2 s

看[知播题解](https://gxyzoj.com/d/hzoj/p/3821/solution/66a3481d1a76fc19c6ada276)。关于此题想说的已经在上面了。

## T3 p

这题一眼就看得出来就是 P2056 的变式,只不过加了一个区间查询而已。P2056 的做法很多,如果我当时能在做那道题的时候看到线段树维护直径这个方法,也不至于这次死调括号序列……不过线段树维护直径**只能用于非负权边**,类似 Dijkstra。

首先我们要维护黑点的数量,白点我们是不看的。维护每个点的颜色,一旦有黑点加入,就要更新。此题相较于 P2056 增加了区间查询,所以需要用线段树或树状数组维护区间。由于是单点修改、区间查询,选择树状数组是最经济的方式。*主要还是被卡空间卡怕了。*

我们的线段树是维护区间黑点之间的直径的。每加入一个黑点,就在线段树上做修改。这里有一个结论:对于两个点集 $S,T$,他们的直径集合分别为 $f(S),f(T)$,则有
$$
f(S\cup T)\subseteq f(S)\cup f(T)
$$
换句~~人~~话说,假设 $S$ 的直径端点为 $u_1,v_1$,$T$ 的直径端点为 $u_2,v_2$,则 $S\cup T$ 的直径端点仅可能存在于 $u_1,v_1,u_2,v_2$ 这四个点中的任意两个中。证明很简单。那么我们就可以用线段树维护一个区间的 $u,v$ 和 $\textit{dis}$,其中 $\textit{dis}$ 表示 $\text{dis}(u,v)$,也就是直径。区间合并的时候在左右儿子的 $u,v$ 中四选二取 $\textit{dis}$ 的最大值即可。

注意,$\text{dis}(u,v)$ 的计算需要用到 LCA 的计算。这里倍增由于自带一个 $\log$ 的常数,套在线段树里会 **TLE 地死死的**,所以请务必使用树剖或 ST 表求 LCA,我更倾向于树剖求,好写好调好背。

## T4 [[ARC096E] Everything on It](https://www.luogu.com.cn/problem/AT_arc096_c)

### 题意

对于集合 $\{1,2,\dots,n\}$,求它的子集族中,有多少个子集满足:

1. 任意两个子集互不相同;
2. $\forall i\in[1,n]$,$i$ 都在其中出现了至少 $2$ 次。

### 解析

这题就涉及到我还不会的知识了:**斯特林数**。咱先从头来看这个题。

设 $\textit{dp}_i$ 表示已经有确定的 $i$ 种元素不合法,其它 $(n-i)$ 种元素任意的方案数,容斥,答案为
$$
\sum_{i=0}^n(-1)^n\binom ni\textit{dp}_i
$$
既然 $\textit{dp}_i$ 中的元素被鲜明地分成了两类,设 $f_i$ 表示确定的 $i$ 种元素不合法元素的方案数,有
$$
\textit{dp}_i=2^{2^{n-i}}f_i
$$
其中 $2^{2^{n-i}}$ 意为:$(n-i)$ 种元素构成了 $2^{n-i}$ 个集合,而这些集合又作为元素构成了 $2^{2^{n-i}}$ 个集合。这里求解这个需要用到欧拉定理,转化为求解 $2^{2^{n-i}\bmod\mathrm{MOD-1}}\bmod\mathrm{MOD}$。

现在压力给到求 $f_i$,考虑这 $i$ 个元素分别放到了哪些集合里,而表示**将 $\boldsymbol i$ 个元素放到 $\boldsymbol j$ 个集合里的方案数就是第二类斯特林数的定义**。它的递推式为:
$$
{i\brace j}={i-1\brace j-1}+j{i-1\brace j}
$$
但只用斯特林数递推略有问题,由于限制是不多于一次,意味着可能存在一些数没有出现过。我们考虑新加入一个集合当做 “垃圾堆”,该集合中的数即为没有出现的数。但是还有一个问题:“垃圾堆” 可能为空。所以我们新加入一个数字 $0$,$0$ 所在集合即为垃圾堆。

所以有
$$
f_i=\sum_{j=0}^n\left(2^{n-i}\right)^j{i+1\brace j+1}
$$
层层回代,得到答案式
$$
\textit{ans}=\sum_{i=0}^n(-1)^i\cdot2^{2^{n-i}}\binom ni\sum_{j=0}^i{i+1\brace j+1}\cdot\left(2^{n-i}\right)^j
$$

### 评价

写的很仓促,不完全理解。

# 7/27

## 总结

不是很理想。答题节奏有点小问题,前 1/3 场在怼 T4 的打表,最后也就比一般人多打了 4pts。然后后 2/3 场一直在怼 T3 的组合数学,结果还没 CQL 的暴力 DP 高。最后 T1 的 20pts 暴力和 T2 的 20pts 暴力拢共只拿了 10pts,rnk14 草草收场。

- T1 看到概率期望心里很慌,也许是好长时间没打了导致忘了。其实 50pts 的 DP 还是很好想的,如果此时的我是刚刷完不少概率期望的我也许能打出来(我记得有段时间特爱考概率期望)。再往后 80pts 的矩阵快速幂不是我的强项,考场上真正拿到 >50pts 的人为零,所以也不做要求了。
- T2 其实比较温和(听 WZW 大佬讲过之后觉得),但考场上能推出来的难度可能真的只有 WZW。不过 20pts 的暴力太可惜了,最重要的是我的代码 *CE* 了,因为**毫秒级别随机数不能在 Linux 下使用**。这点其实能在 CSP 之前发现是非常非常庆幸的,别到时候一个 `_timeb` 把一整套代码全葬送了。
- T3 一眼高级组合数学,就一直死怼,不过没想到卡特兰数,因为打太久了忘了。$\textit{typ}=1$ 的 25pts 非常显然,$\textit{typ}=2$ 的 DP 推了不少时间,$\textit{typ}=3$ 推了很久,最后 $\textit{typ}=0$ 没时间推了。原本 75pts 还变成了 60pts,因为**没开 long long**。
- T4 一眼不会,*STZ 大佬场切非常佩服*,我就剩下打表了,最后比一般人多打了 4pts,其实还能再多打 2pts,不过这不重要。

总之,*这场比赛是非常有教育意义的*。**概率期望、矩阵快速幂、组合数学**,这些都是考出来非常拉分的知识点,必须掌握。

## T1 随

- 10pts:$\textit{mod}=2$,直接输出 `1` 拿下;
- 20pts:$n=1$,直接输出 $a_1^m\bmod\textit{mod}$ 拿下;  
  *还是答题节奏的问题,考场上没用快速幂导致这 10pts 失手。*
- 50pts:考虑 DP。设 $\textit{dp}_{i,j}$ 表示考虑到第 $i$ 个数,当前结果为 $j$ 时的答案,有 DP 方程
  $$
  \textit{dp}(i,j)=
  $$

## T2 单

- 10pts:经典 LCA 暴力求树上距离;
- 20pts:$a_i\le20$,考虑暴力枚举;
- 40pts:考虑 up-and-down 求 $b$ 数组。先一遍 DFS 求出一个 $\textit{sum}$ 数组,$\textit{sum}_i$ 表示 $i$ 的子树的点权和。随后我们就有一个方程:
  $$
  b_i=b_\textit{fa}-\textit{sum}_i+(\textit{sum}_\textit{rt}-\textit{sum}_i)=b_\textit{fa}+\textit{sum}_\textit{rt}-2\textit{sum}_i
  $$
  我们发现我们只需要知道 $b_\textit{rt}$ 就可以求出整棵树的 $b$ 数组,而 $b_\textit{rt}=\sum_{i=2}^n\textit{sum}_i$,进而得解。
- 100pts:也就是 $t=1$ 的情况,需要我们通过 $b$ 数组求解 $a$ 数组。乍一看很难,关键点就在于我们刚推出的那个式子:
  $$
  b_i=b_\textit{fa}+\textit{sum}_\textit{rt}-2\textit{sum}_i
  $$
  移项得
  $$
  b_i-b_\textit{fa}=\textit{sum}_\textit{rt}-2\textit{sum}_i
  $$
  只要求出 $\textit{sum}_\textit{rt}$,$b$ 是我们已知的,所以就可以求出 $\textit{sum}_i$,进而差分出 $a_i$。

  每个除根节点以外的节点都有这个式子,也就是 $(n-1)$ 个这样的式子,**把它们求和:**
  $$
  \sum_{i=1}^nk_ib_i=(n-1)\textit{sum}_\textit{rt}-2\sum_{i=2}^n\textit{sum}_i
  $$
  其中 $k_i$ 是根据树的结构决定的系数,我们只需一遍 DFS,每遍历到一个节点给 $k_i\gets k_i+1,k_\textit{fa}\gets k_\textit{fa}-1$ 就可以求出。移项有
  $$
  \textit{sum}_\textit{rt}=\frac1{n-1}\left(\sum_{i=1}^nk_ib_i+2\sum_{i=2}^n\textit{sum}_i\right)
  $$
  后面那个看起来很难求,但请注意前面我们已经推过的某一式子,可知:**这个玩意就是 $\boldsymbol{b_1}$**。

  然后就求出来了。

### 评价

WZW 大佬真的非常善于推式子啊。

## T3 题

高级组合数学,这题的部分分非常明显,主要在于求组合数。*其实想到卡特兰数又能如何呢,你卡特兰数还不是得拿组合数求。不过如果能看出来卡特兰数,不失为一种猜结论的方法。*

主要学习的还是 CQL 的 DP,这个技巧非常实用,定义 $f_{i,j,k}$ 为 $i$ 步之后走到 $(j,k)$ 的方案数。但这是 $O(n^3)$ 的,要做到 $O(n^2)$ 我们看到 CQL 同志在 $\textit{typ}=2$ 时的转移:

```cpp
dp[0][1000] = 1;
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= 4000; j++) {
        if (j == 1000)
            dp[i][j] = (dp[i - 1][999] + dp[i - 1][1001] + dp[i - 1][2001] + dp[i - 1][3001]) % mod;
        else if (j == 999)
            dp[i][j] = (dp[i - 1][1000] + dp[i - 1][998]) % mod;
        else if (j == 1001)
            dp[i][j] = (dp[i - 1][1000] + dp[i - 1][1002]) % mod;
        else if (j == 2001)
            dp[i][j] = (dp[i - 1][1000] + dp[i - 1][2002]) % mod;
        else if (j == 3001)
            dp[i][j] = (dp[i - 1][1000] + dp[i - 1][3002]) % mod;
        else 
            dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j + 1]) % mod;
    }
cout << dp[n][1000] << "\n";

当时他讲了,不过现在也不太记得了。

T4 DP搬运工1

比较新颖的 DP 转移方式,叫预设型 DP,它的一个点在于对于一个序列 \(\texttt{[....1..23...4......]}\) 的情况,中间的 \(\texttt .\) 可以有无数个,我们不关心有多少个空位,只关心能否将新元素插入任意一个空位中,具体插入后空位的情况我们再分讨

当然,考试上几乎没有人能想出这个转移的,更多的人还是采取暴力 + 打表的方式,毕竟能骗两分是两分。

7/29

这场比赛是稳定的,有挂分但不多,保住了祖传 rnk8 的好成绩,但 T4 的挂分并非不可查,还是要多检查啊。

T1 [ARC148A] mod M

简单题,易发现模数为 \(2\) 的时候肯定是最优的,所以全输 12 就有可观的分数经测试,全输 1 拿 59pts,2 拿 41pts,随机输刚好 50pts。

但在某些情况下模数为 \(2\) 并不优,当且仅当这些数对某一个模数是同余的。所以排序然后 gcd 判一下相邻数之间的差即可。

T2 [ARC107D] Number of Multisets

T2 是臭名昭著的 \(998244353\),也是 DP 题。转移很神仙,具体地,如果所有数凑成了 \(\boldsymbol j\),那么只需全部除以二就能凑成 \(\boldsymbol{j/2}\),所以 \(f_{i,j}\) 表示 \(i\) 个数和为 \(j\) 的方案数,转移:

\[f_{i,j}=f_{i-1,j-1}+f_{i,j\times2} \]

T3 [ARC149D] Simultaneous Sugoroku

  • 暴力的 47pts 很可观;
  • 此题的 trick:将移动波特转化为移动原点,同时观察移动中在原点两侧的不变的量及时剪枝,从而减少枚举。

T4 [ICPC2018 WF] Triangles

  • 这题让我想到南蛮图腾,不过显然比那题单纯的模拟要难得多;
  • 树状数组维护的做法很 D,考场上能保证自己的暴力不写挂就不错了;
  • 暴力有 50pts。

7/30

炸废了。还是考场上不够细心,更多地去推难题的高级部分分 / 正解,从而简单题上挂了分。

T1 [ARC124B] XOR Matching 2

不难,我们发现要想减少枚举,就从枚举结果入手,此题的结果只能是 \(n\) 的复杂度,分别用 \(a_1\) 去异或 \(b_i\),然后暴力检查这个结果能否匹配上剩余的 \(b_j~(j\ne i)\),如果能匹配上答案就累加,一定一定要对答案去重。

具体可以对 \(a,b\) 数组都排序,可以减少暴力检查的复杂度。

T2 S

很可惜的一道 DP,推了最长时间,也是挂分最多……

显然的一点是,抽屉原理,如果一个球的数量超过了 \(\lceil n/2\rceil\),那一定是无解。然后考虑每个球给总答案的贡献,设 \(\textit{dp}_{i,j,k}\) 表示三种颜色的球都填多少了,但是这样没法转移,因为这题还要求同种颜色不相邻,那么再设一维变成 \(\textit{dp}_{i,j,k,0/1/2}\) 表示最后填的球的颜色。因为同种颜色是不可能交换的,所以每个球要交换到 “不丑” 需要走逆序对的数量,而这个逆序对我们可以通过处理一个 \(\textit{sum}_{0/1/2,i}\) 表示前 \(i\) 个位置三种颜色球的总和、\(\textit{pos}_{0/1/2,i}\) 表示总和为 \(i\) 时三种颜色的球的位置,然后转移:

\[\textit{dp}_{i,j,k,0}=\min\{\min(\textit{dp}_{i-1,j,k,1},\textit{dp}_{i-1,j,k,2})+\textit{sum}_{1,\textit{pos}_{0,i}}-j+\textit{sum}_{2,\textit{pos}_{0,i}}-k\} \]

其他情况的转移同理。

考完试检查发现还是转移的地方有漏,加上一定要滚动数组优化空间

T3 [ARC124E] Pass to Next

很 D 的一道题。

T4 [JOI 2020 Final] 火事 / 火事 (Fire)

我参考的是这篇的乱搞做法。相关正在研究。具体而言,用 list 维护递减段是很新颖的,以前对 list 不是很了解,以后可以多了解一些奇技淫巧。

7/31

rnk2,刷新纪录了。不过 bobo 说考的好是因为打的顺,所以也没什么可骄傲的。

T1 黑客

枚举答案,老套路了。我们枚举最终约分好的分子和分母,用 \(A,B,C,D\) 分别除以它们,得到两段区间,取区间的交集累加答案,时间复杂度 \(O(999^2)\)。

T2 密码技术

上来先看题:又是一道 \(998244353\)……

不过再一看数据范围:\(n\le50\),这不是标准乱搞么。我们仔细观察这个题,首先我们知道,如果有 \(\boldsymbol x\) 行(列)可以交换,那么它们会给答案有 \(\boldsymbol{x!}\) 的贡献。我们又发现,行的方案数和列的方案数是互不影响的。所以这题看似 \(998244353\),实际上就是很简单的计数。

接下来看怎么统计。我们重点是要知道能相互交换的行(列)数。既然数据这么水,对于任意两行(列)能否交换我们直接暴力 \(O(n^3)\) 求即可。我们发现若 \(a,b\) 两行能交换,\(b,c\) 两行能交换,那么 \(a,b,c\) 三行都能交换。所以就很自然地并查集处理。然后这题就做完了。

T3 [HNOI2015] 亚瑟王

出题人包装题面的能力是越来越高了,不过还是被我搜样例找到了原题。

45pts 的状压本以为稳了,没想到需要优化,否则只有 11pts。具体而言,我们原本是要枚举 \(s\in[0,2^n-1]\),但其实我们没有必要枚举这么多,稍微存一下每个状态的点就可以优化。

标签:总结,cnt,DP,int,sum,近期,模拟,textit,dp
From: https://www.cnblogs.com/laoshan-plus/p/18365149

相关文章

  • 《软件测试》黑书全22章笔记总结——软测新手小白必读
    一、软件测试综述1.第一章:软件测试的背景1.1软件缺陷只有至少满足下列5个规则之一才称为发生了一个软件缺陷软件未实现产品说明书要求的功能软件出现了产品说明书指明不应该出现的错误软件实现了产品说明书未提到的功能软件未实现产品说明书虽未明确提及但应该实现的......
  • Transformer问题总结及实现
    目录前提:注意:以下对于优化的问题,要回答这个问题:前一种方法的局限性在哪里,优化的方法是怎么进行优化的?(未完全解决)Step1:关于Transformer的疑问Step2:关于Transformer各层的实现(未解决)2.1:Encoder细节2.2:Decoder细节2.3:怎么用Transformer提升Kaggle平台的House_pricing竞赛?......
  • 原生微信小程序笔记完整总结4.0
       ......
  • 『模拟赛』暑假集训CSP提高模拟23
    Rank玩蓝图玩的A.进击的巨人(原题都是牛客的,没号所以不挂了)赛事看到概率期望一眼润,但是又可惜暴力分,遂打(最坏情况下)\(\mathcal{O(n^2)}\)暴力,结果很给力啊,调出来小样例后大样例嗖的一下就过了,惊喜了属于是,喜提100pts。事实上跑这么快是因为0的数量很平均,导致复杂度大......
  • [赛记] 暑假集训CSP提高模拟22 23
    连通块66pts老套路,删边改加边;但改完以后不知道怎么求最长路径了,当时也想到了维护直径,但不知道咋干;具体地,用并查集维护连通性,每次合并时需要维护新的直径,不难发现,新的直径的两个端点一定在原来的两个直径的四个端点中选;于是只有六种情况,枚举一下即可;我们要直径有啥用呢?当我们......
  • 【面试宝典】java基础面试题总结[上]
    一、Java中有几种基本数据类型?各占多少字节?在Java中基本数据类型有8个,占用的字节分别是整型byte(1个字节)、short(2个字节)、int(4个字节)、long(8个字节);浮点型float(4个字节)、double(8个字节);布尔类型boolean;字符类型char(2个字节)。二、String类能被继承吗?为什么?Stri......
  • [考试记录] 2024.8.17 csp-s模拟赛21
    T1Set解析思考+组合题场上只能想到暴力01背包再加上bitset优化,很好打。本应该有60pts(?或者更多),不曾想由于spj的一些未知原因喜提systemerror,全部cancelled。喜提0pts。......
  • 2024暑假第七周总结
    学生管理系统基础构建需求分析需求:实现学生的基本管理功能,包括:添加、删除、更新、查询学生信息设计:确定系统的主要模块,包括:学生类、学生管理类、用户界面等。学生类属性:学生学号、姓名、年龄、班级方法:获取和设置属性值、展示学生信息学生管理类功能:添加、删除、更新、查......
  • cf:Removals Game(博弈论模拟),Black Circles(距离)
    RemovalsGame题目爱丽丝得到了[1,2,...,n]的置换al,a2,...,a,鲍勃得到了[1,2...,n],的另一个置换b1,b2,...在每个转折中,下列事件按顺序发生:爱丽丝选择数组中的第一个或最后一个元素,并从数组中移除它;鲍勃选择数组中的第一个或最后一个元素,并将它从数组中移除。游戏继续n-1轮,之后两个......
  • 8.17周总结
    本周学习的东西比较少,因为也要准备开学考试,本周将老师所留的PTA程序设计实验进行了结尾,并对CSS代码中的三个重要方面进行了学习,常规流,浮动。对于常规流,就是属于我们平常所写的一些代码,在这里我们了解了盒子的包含块,等于其父元素的内容盒;我学习了块盒,对于在块盒中,我知道了每个块盒......