2025北京知码狐集训

集训日记

这篇文章可能很长(尤其从Day 1开始)

Day 0

火车杯

在火车上举行了哈三种集训队互测“火车杯”比赛,WYQ逆天发言:幸好没坐飞机
image
领跑全火车成功
注意到F题的满分是2pts
赛后举行了猜出题人环节
3a1919f8da504c248d44b7d1849b5c3c
并且我成功猜对了所有人
最后对所有人选出的题评价,而我的编译原理大模拟成功获得了Down Vote,-3技术点,大好优势没了。
结局是yyy胜出,lyd垫底。
但是高铁弄得我耳朵疼,以至于下车后相当长一段时间听力暂时下降,不过恢复了。
哦对了,火车杯比赛的后期其实所有人都在颓,我在看小说 \(or\) 睡觉,他们在打麻将。

渐渐的,我睡着了。列车缓缓进站了。

麻将

到了宿舍开始颓麻将,注意到新手不会玩
但是在南风场成功和牌得到了大满贯
背景音乐坐忘道真的是太好玩了
麻将不知道写什么好,最终就写到这吧。

Day 1

早上

谁扔的闪!

开营大会

讲述了一些事物。

还有好玩的解压设备和乒乓球场
yyy狂喜。
注意到我忘记拿鼠标垫了,导致我的鼠标寸步难行TOT
为什么下课不能把笔记本带回宿舍。

专题:数学

A题压力,B题暴毙,C题沙必,D题退役
一想到我到了高二高三将要面对这些东西用于高考我就想哭。

A

我们称满足以下所有条件的长度为 \(N\) 的整数集合序列 \(S=(S_1,S_2,\dots,S_N)\) 为“素晴らしい集合の列”:

  • \(S_i\) 是仅包含 \(1\)\(M\) 之间整数的集合(可以是空集)。\((1 \le i \le N)\)

  • 对于每个 \(1 \le i \le N-1\)\(S_i\)\(S_{i+1}\) 的对称差中恰好有 \(1\) 个元素。
    这里,定义一个“素晴らしい集合の列” \(S\) 的得分为 \(\displaystyle\prod_{i=1}^{M}\)\(S_1,S_2,\dots,S_N\) 中包含 \(i\) 的集合的个数)。
    请你求出所有“素晴らしい集合の列”的得分之和,结果对 \(998244353\) 取模。
    ?这啥。
    目前来看每种元素必须要出现一次,并且对称差(两个集合的交集对于两个集合的并集的补集)包含元素恰好为1
    一开始的思路是必须要有一个满集合,后来发现并非,比如如下的hack:

3 3

我们很容易就可以得到:

{1, 2} {2} {2, 3}

这样的数列,他的得分是 \(1 \times 3 \times 1 = 3\)
这道题的重点在于要数出每个整数集合序列的得分,那么可以看到这样的性质:
对于每种元素至少出现了 \(1\) 次,至多出现了 \(N\) 次且每次必然有 \([1, N - 1]\) 个元素出现次数不是N
打表找规律:

LIST
1 1 :1
1 2 :1
1 3 :1
1 4 :1
1 5 :1
1 6 :1
1 7 :1
1 8 :1
1 9 :1
1 10 :1
2 1 :2
2 2 :8
2 3 :24
2 4 :64
2 5 :160
2 6 :384
2 7 :896
2 8 :2048
2 9 :4608
2 10 :10240
3 1 :3
3 2 :36
3 3 :243
3 4 :1296
3 5 :6075
3 6 :26244
3 7 :107163
3 8 :419904
3 9 :1594323
3 10 :5904900
4 1 :4
4 2 :128
4 3 :1728
4 4 :16384
4 5 :128000
4 6 :884736
4 7 :5619712
4 8 :33554432
4 9 :191102976
4 10 :50331647
5 1 :5
5 2 :400
5 3 :10125
5 4 :160000
5 5 :1953125
5 6 :20250000
5 7 :187578125
5 8 :601755647
5 9 :835520889
5 10 :826547759
6 1 :6
6 2 :1152
6 3 :52488
6 4 :1327104
6 5 :24300000
6 6 :362797056
6 7 :711906940
6 8 :134217673
6 9 :124236716
6 10 :251553879
7 1 :7
7 2 :3136
7 3 :250047
7 4 :9834496
7 5 :262609375
7 6 :497809979
7 7 :59308166
7 8 :864287255
7 9 :277822188
7 10 :47942884
8 1 :8
8 2 :8192
8 3 :1119744
8 4 :67108864
8 5 :563511294
8 6 :511705015
8 7 :132118846
8 8 :251622994
8 9 :267792368
8 10 :509337394
9 1 :9
9 2 :20736
9 3 :4782969
9 4 :429981696
9 5 :106395506
9 6 :186355074
9 7 :357199956
9 8 :301266414
9 9 :437339833
9 10 :976071784
10 1 :10
10 2 :51200
10 3 :19683000
10 4 :624951294
10 5 :654851165
10 6 :419256465
10 7 :781521515
10 8 :137549566
10 9 :862692126
10 10 :172998509

我们动用了世上最纯粹的力量:枚举!
轻而易举的发现了这样的一个式子:

\[ans = n ^ m \times m ^ {n - 1} \bmod 998244353 \]

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int mod = 998244353;
int n, m;
set<int> full;
int fpow(int a, int b) {
int res = 1;
while (b) {
    if (b & 1) res = res * a % mod;
    a = a * a % mod;
    b >>= 1;
}
return res;
}
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> m;
cout << fpow(n, m) * fpow(m, n - 1) % mod << endl;
return 0;
}

成功通过。

L(原)

注意到有 \(inf\) 个人过了 L ,跟榜!
AtCoder 王国出售 \(N\) 种类的章鱼烧。第 \(i\) 种章鱼烧的价格是 \(A_i\) 日元。
高桥一共买了 \(1\) 个以上的章鱼烧。这个时候,也允许买多个同样的章鱼烧。
高桥君可以支付的金额中,请从便宜的一方寻求第 \(K\) 的金额。但是,如果存在多个支付相同金额的方法,则只计算$ 1 $个。
注意到英文原文是:

Takahashi will buy at least one takoyaki in total.
显然这是翻译问题,应该是 \(1\) 个及以上。
Find the K-th lowest price that Takahashi may pay. Here, if there are multiple sets of takoyakis that cost the same price, the price is counted only once.
TMD, 从小到大排序能不能好好说!
这道题第一反应是背包DP,但是显然今天是数学专题
注意到可以把所有的数字扔进一个集合,然后循环 $ k - 1 $ 次取出集合中最小值并与每一个 \(a\) 加一下
最终的答案就是最小值。

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int n, k;
int a[11];
set<int> st;
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
    cin >> a[i];
    st.insert(a[i]);
}
while (k-- != 1) {
    int p = *st.cbegin();
    for (int i = 1; i <= n; i++)
        st.insert(a[i] + p);
    st.erase(p);
}
cout << *st.cbegin() << endl;
return 0;
}

AtCoder挂了,但是应该是过了。
好的我过了。

B

定义一个矩阵 \(F\),其中第一行和第一列是给定的,计算矩阵方法如下:
矩阵的第一列是序列 \(l\)
\(F[k,1]=\) \(l _ k\)
矩阵的第一行是序列 \(t\)
\(F[1,k]=\) \(t _ k\)
其他元素使用给定的递归公式进行计算:
\(F[i,j]=a \times F[i,j-1]+b \times F[i-1,j]+c\)
现在要求找求出 \(F[n,n]\)\(10^6+3\) 的值。
这道题卡了我半天,只能推出来 \(a\)\(b\) 还是错的。挂掉了,去看别的题。
注意到dyx老师一骑绝尘推出式子,当然给我式子我都看不懂。
最后还是出来了:

#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int mod=1e6+3, maxn = 2e5+5;
int n, a, b, c;
int l[maxn], t[maxn], fac[maxn * 2], inv[maxn * 2];
int ans, now;
int C(int n, int m) {
return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
int fpow(int a, int b) {
int res = 1;
while (b) {
    if (b & 1) res = res * a % mod;
    a = a * a % mod;
    b >>= 1;
}
return res;
}
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> a >> b >> c;
for (int i = 1; i <= n; i++)
    cin >> l[i];
for (int i = 1; i <= n; i++)
    cin >> t[i];
fac[0] = 1;
for (int i =1; i <= n * 2; i++)
    fac[i] = fac[i - 1] * i % mod;
inv[0] = 1;
inv[2 * n] = fpow(fac[n * 2], mod - 2);
for (int i = n * 2 - 1; i > 0; i--)
    inv[i] = inv[i + 1] * (i + 1) % mod;
for (int i = 2; i <= n; i++)
    ans = (ans + C(2 * n - i - 2, n - i) * fpow(a, n - 1) % mod * fpow(b, n - i) % mod * l[i] % mod) % mod;
for (int i = 2; i <= n; i++)
    ans = (ans + C(2 * n - i - 2, n - i) * fpow(b, n - 1) % mod * fpow(a, n - i) % mod * t[i] % mod) % mod;
for (int i = 0; i <= n - 2; i++)
    ans = (ans + fpow(a + b, i) * c % mod) % mod;
for (int i = 1; i <= n - 2; i++)
    now = (now + C(n - 1, i) * fpow(a, i) % mod * fpow(b, n - i - 1) % mod) % mod;
for (int i = n - 1; i <= (n - 2) * 2; i++) {
    if (i > n - 1) {
        now = now * ( a + b ) % mod;
        now = (now - C(i - 1, n - 2) * fpow(a, n - 1) % mod * fpow(b, i - n + 1) + mod) % mod;
        now = (now - C(i - 1, n - 2) * fpow(b, n - 1) % mod * fpow(a, i - n + 1) + mod) % mod;
    }
    ans = (ans + now * c % mod + mod) % mod;
}
cout << ans << endl;
return 0;
}

中午

到底还是把电脑拿回宿舍了,在调 KMP ,并没有学会。
顺手把自己的人工智能复制体调试了一下,现在他更像我了。
然而他也不会KMP。

下午

讲题老师:这些题并没有很难
这让我想起来了我的生物老师。
云里雾里的听,大概记下来课程内容吧

A

这题讲的时候没听懂,反正最后答案是我的式子……

B

首先考虑 \(c = 0\) 的时候,每一个:

\[ F_{k, i} \rightarrow{} F_{n, n} \]

\[F_{i, k} \rightarrow{} F_{n, n} \]

最后可以拆贡献为一个网格图,一共有 \(n - k\) 个向下,\(n - 1\)个向右
可以得到这样的一个对于 \(f_k\) 的递推式:

\[\sum_k l_k \times a^{n - 1} \times b^{n - k} \times \binom{2n - 1 - k}{n - 1} + \sum_k t_k \times b^{n - 1} \times a^{n - k} \times \binom{2n - 1 - k}{n - 1} + \sum_{2 \le i \le n, 2 \le j \le n} c \times a^{n - 1} \times b^{n - j} \times \binom{2n - i - j}{n - i} \]

Latex不熟练,将就看吧,忘记怎么换行了。
这道题非常简单,总之怎么处理都可以的
啊?
然后就是一堆处理方法,我使用的是WYQ大法(因为以前看WYQ写数学题就这么写):硬算,数学怎么算我就怎么算。最后肯定还是对了的。

C

原题:https://www.luogu.com.cn/problem/AT_arc139_d

\[\sum_i a_i = \sum_i (\sum_{x \ge 1}^{+\infty} [x \le a_i] ) \]

……这道题是DP,维护和,说了一些式子没跟上…… 我是fw
总之他就是发现了这个东西是一个 DAG ,只需要知道要枚举几次就好了。
注意到了解了新的数学符号:艾弗森括号,可以理解为if,满足条件为1,反之为0。

老师,c是什么?
有一位神秘小朋友一直在提问,非常好,有勇气
这让我捡漏学会了01化

01化 (重点专题)

应该是这样的
把各种条件坍塌成 \(0\)\(1\) ,比如说排序:

\[[x \le i] \]

可以把数列按照这个条件来判断放在左和右。
可以通过归纳来发现这个判断的方法,枚举并坍塌状态就可以了。
听完了发现这就是状态压缩…… E
但是这个思想还是比较有用的,通过压缩状态来判断以减小复杂度
我也算是学会状态压缩了!

D

直接贴题:https://www.luogu.com.cn/problem/CF1097G

大家都知道泰勒展开么,小学生必备
讲的什么没听懂,总之我知道我可能真的需要去学斯特林数(尤其是第二类)
我是谁,我在哪,我要干什么,我应该干什么,我到底要做什么,他在说什么……
这样我们就把简单的四道题讲完了。

E (重点:二项式反演 & 概率)

题目:https://www.luogu.com.cn/problem/P5400

首先大家都会一个东西叫二项式反演吧

二项式反演(重点)

我好像真的会一点!
二项式反演的本质是两面展开:

\[\begin {aligned} f &=& g \times e^x \\ g &=& f \times e^{-x} \end {aligned} \]

二项式反演是一种反演形式,常用于通过“指定某若干个”求“恰好有若干个”的问题。
他的本质上就是容斥原理,大概可以这样:

\[\begin{aligned} g_i &=& \sum_{j=0}^i \binom{i}{j} f_i \\ f_i &=& \sum_{j=0}^i (-1)^{j-i} \binom{i}{j} g_j \end{aligned} \]

image
表示

概率(重点)

概率的算法是什么呢?只需要这样:

\[\text{chance} = \frac{\text{Expected Possibilities}}{\text{All Possibilities}} \]

在模运算意义下的概率直接用逆元算就好了。我打算今天晚上把KMP弄明白之后开始刷数学题。

F

这道题是难题
听不懂,真的听不懂,我去研究二项式反演了。
后面的题我就不记了,如果有什么重点的知识就记录一下。
好的二项式反演会了,这时候开始研究KMP。

KMP(AI版, 待吸收)

我的人工智能智能体写的代码在输出上有问题,可是核心功能是对的!

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
#define next frn3847fh2
using namespace std;
constexpr int maxn = 1e6 + 10;
string s, t;
int next[maxn], len_s, len_t;
void get_next() {
  int j = 0;
  for (int i = 2; i <= len_t; ++i) {
      while (j && t[i] != t[j + 1]) j = next[j];
      if (t[i] == t[j + 1]) ++j;
      next[i] = j;
  }
}
void kmp() {
  int j = 0;
  for (int i = 1; i <= len_s; ++i) {
      while (j && s[i] != t[j + 1]) j = next[j];
      if (s[i] == t[j + 1]) ++j;
      if (j == len_t) {
          cout << i - len_t + 1 << endl;
          j = next[j];
      }
  }
}
signed main() {
  cin.tie(0), cout.tie(0);
  ios::sync_with_stdio(0);
  cin >> s >> t;
  s = " " + s;
  t = " " + t;
  len_s = s.size() - 1;
  len_t = t.size() - 1;
  get_next();
  kmp();
  for (int i = 1; i <= len_t; ++i) {
      cout << next[i] << " ";
  }
  cout << endl;
  return 0;
}

我们注意到字符串在匹配的时候采用了错位匹配生成next数组,然后回退。
我可能需要一晚上消化吸收,现在我应该去刷Trie树板子和线段树板子以及分块板子。
打完KMP发现L题换题了!
掉榜了。没事,后面我应该是听不懂,打自己应该做的吧。

板刷

字典树
AC
分块
AC
线段树
AC

反射容斥(重点)

现在有一个数 \(b_0\) 和一个大小为 \(n\) 的数列 \(a\),对于每个 \(a_i\) 不超过 \(m\)
生成大小为 \(n\) 的数列 \(b\) 满足这样的条件:

\[\forall b_i \in b, \lvert b_i - b_{i - 1} \rvert = 1, b_i \neq a_i \\ \forall b_i \in b, b_i \ge 0 \\ \forall a_i \in a, a_i \in [1, m] \]

image
那么这道题就是:
image
大概是这样的。

单位根反演(重点)

注意到性质:

\[998244353 = 199 \times 2^{23} + 1 \]

d2e1b1e5c5dfae880d2068fd60a5f101
感谢WYQ提供的图片。不愧是哈三中前二十的男人!
单位根反演是一种利用单位根性质将整除条件(如k整除n)转化为求和形式的数学技巧,常见于组合数学、数论变换和算法优化中。其核心公式基于单位根的循环性和求和性质。
单位根反演的基本形式为:

\[[k \mid n] = \frac{1}{k} \sum_{i=0}^{k-1} \omega_k^{in} \]

其中,\(\omega_k = e^{2\pi i / k}\) 是k次单位根(即\(\omega_k^k = 1)\)\([k \mid n]\) 是指示函数,当k整除n时值为1,否则为0。‌

分治FFT

FFT(快速傅里叶变换)

1. 多项式的奇偶拆分
对于 \(n - 1\) 次多项式 \(f(x) = \sum_{i=0}^{n-1} a_ix^i\),按下标奇偶性拆分为两个子多项式:

  • 偶次项子多项式:

\[\begin {aligned} G(x) &=& a_0 + a_2x + a_4x^2 + \dots \\ &=& \sum_{i = 0}^{i \le \frac{n}{2}} a_{2i}x^i \end {aligned} \]

  • 奇次项子多项式:

\[\begin {aligned} H(x) &=& a_1 + a_3x + a_5x^2 + \dots \\ &=& \sum_{i=0}^{i \le \frac{n}{2}} a_{2i+1}x^i \end {aligned} \]

因此,我们可以表示原来的多项式为:

\[f(x) = G(x^2) + x \times H(x^2) \]

2. 单位根\(\omega\)的引入(蝶形运算的核心)
为了让子问题快速合并,FFT引入了n次单位根 \(\omega_n\) ,满足\(w_n^n = 1\),用欧拉公式可以得到:

\[\omega_n = e^{\frac{2\pi i}{n}} = \cos \frac{2\pi}{n} + i\sin\frac{2\pi}{n} \]

3. 分治合并
利用单位根对称性 \(\omega_n^{\frac{n}{2}} = -1\)

\[f(\omega_n^{k + \frac{n}{2}}) = G(\omega_{\frac{n}{2}}^k) - \omega^{k}_{n} \times H(\omega_{\frac{n}{2}}^{k}) \]

这意味着:只要知道了 \(G\)\(H\)\(\omega_{\frac{n}{2}}^k\) 的点值,就可以同时算出 \(f\)\(\omega_n^k\)\(\omega_n^{k + \frac{n}{2}}\)的点值
你听懂了么?反正我没听懂。

晚餐

由于 Day 1 的日程过于坎坷,导致晚饭开始emo
进省队的希望太渺茫了,倒不如原地退役搞文化课,省一等奖也是不错的成绩
回顾人生的过去阶段:

  • 小学从班级后一半冲到第一,到了哈工大附中

  • 初中从班级后一半冲到前15,虽有中考失利缺仍旧靠创新人才来到哈三中

  • CSP-S 20分冲到了NOIP 124分,拿下省一
    或许创新人才就是上帝赐予我的启示吧,除了我谁能想到我能来到南三呢
    如今我险些丢掉了少年的狂傲,这是不可取的。

人之功成皆凭己为,何来侥幸之说
特码的,老子拼了
必须冲金牌

晚自习

注意到NOIP 2025终于公布名单了
777127c511c05492e803719fbdfb1595

KMP(重点)

终于学会了!

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
#define next hr428tu1398
using namespace std;
constexpr int maxn = 1e6 + 5;
int next[maxn];
string s1, s2;
void getNext(string s) {
  size_t len = s.size();
  s = ' ' + s;
  int j = 0; // j指针用于匹配小字符串
  for (int i = 2; i <= len; i++) { // i指针只进不退
      while (j && s[j + 1] != s[i])
          j = next[j];
      if (s[i] == s[j + 1]) j++;
      next[i] = j;
  }
}
vector<int> KMP(string s, string t) {
  size_t len = s.size(), tail = t.size();
  vector<int> ans;
  getNext(t);
  s = ' ' + s;
  t = ' ' + t;
  int j = 0;
  for (int i = 1; i <= len; i++) { // 注意不用错位
      while (j && s[i] != t[j + 1])
          j = next[j];
      if (s[i] == t[j + 1]) j++;
      if (j == tail) { // 当j指针走到最后的时候
          ans.push_back(i - tail + 1);
          j = next[j];
      }
  }
  return ans;
}
signed main() {
  cin.tie(0), cout.tie(0);
  ios::sync_with_stdio(0);

  cin >> s1 >> s2;

  auto ans = KMP(s1, s2);

  for (int elem : ans)
      cout << elem << endl;

  for (int i = 1; i <= s2.size(); i++)
      cout << next[i] << ' ';
  cout << endl;

  return 0;
}

第一部分:匹配 \(next\) 数组

通过对字符串本身进行一次错位的 KMP ,记录下对于每个指针 \(i\) 对应的 \(j\)点位置,就可以获得对应长度的 KMP 回溯数组。

第二部分:核心KMP

进行一次 KMP ,依次比对每个位置的字符并用已经生成的 \(next\) 数组进行回溯
当j指针指向字符串的末尾证明匹配成功,加入到 \(ans\) 数组即可。

时间复杂度

一次 KMP 是 \(\mathcal{O}(n)\) 的,因为 \(i\) 指针从头扫到尾,而 \(j\) 指针只是进行了跳跃操作,所以单次 KMP 一定是 \(\mathcal{O}(n)\)
注意到我们进行了两次KMP,所以总复杂度是 \(\mathcal{O}(n + m)\)
刷五遍模板!
image
image
image
image
image
讲课的腾讯会议公屏上赫然出现:

我是大灯泡!

字符串哈希(用于匹配字符串)

我们首先定义这样一个哈希函数 \(hash\) 返回一个数对,处理字符序列 \(s\)

\[hash(s) = ( \sum_{i = 1} s_i \times 128 ^ {i - 1} \bmod ( 10^9+7 ), \sum_{i=1} s_i \bmod (10^9+7) ) \]

然后在移动字符串两端的指针的时候,我们可以通过上一个哈希值和上一个字符串的首字符以及当前字符串的末字符推出新的哈希值,很显然时间复杂度是:\(\mathcal{O}(n)\)

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
#define next fn3uth4356
#define hash fn3ut42tt53
using namespace std;
constexpr int mod = 1e9+7, maxn = 1e6+5;
int tab128[maxn];
int inv;
int fpow(int a, int b) {
  int res = 1;
  while (b) {
      if (b & 1) res = res * a % mod;
      a = a * a % mod;
      b >>= 1;
  }
  return res;
}
int next[maxn];
void getNext(string s) { // 过板子用的,主要练习哈希
  int len = s.size();
  s = ' ' + s;
  int j = 0;
  for (int i = 2; i <= len; i++) {
      while (j && s[i] != s[j + 1])
          j = next[j];
      if (s[i] == s[j + 1])
          j++;
      next[i] = j;
  }
  for (int i = 1; i <= len; i++)
      cout << next[i] << ' ';
  cout << endl;
}
// 哈希函数
pair<int, int> hash(string s) {
  int left = 0, right = 0;
  for (int i = 0; i < s.size(); i++)
      left = ( left + s[i] * tab128[i] ) % mod;
  for (int i = 0; i < s.size(); i++)
      right = ( right + s[i] ) % mod;
  return {left, right};
}
vector<int> match(string s, string t) {
  vector<int> ans;
  auto standard = hash(t);
  auto current = hash(s.substr(0, t.size()));
  if (current == standard)
      ans.push_back(1);

  for (int i = 1; i <= s.size() - t.size(); i++) {
      current.first = ( ( current.first - s[i-1] ) * inv % mod + s[i + t.size() - 1] * tab128[t.size() - 1] % mod ) % mod;
      current.second = ( current.second - s[i - 1] + s[i + t.size() - 1] ) % mod;
      if (current == standard)
          ans.push_back(i + 1);
  }
  return ans;
}
string s1, s2;
signed main() {
  tab128[0] = 1, inv = fpow(128, mod - 2);

  for (int i = 1; i < maxn; i++)
      tab128[i] = 128 * tab128[i - 1] % mod;

  cin.tie(0), cout.tie(0);
  ios::sync_with_stdio(0);

  cin >> s1 >> s2;
  auto ans = match(s1, s2);
  for (int elem : ans)
      cout << elem << endl;

  getNext(s2);

  return 0;
}

学会了字符串哈希!
image
注意到我们设计的哈希函数足够安全(因为是双哈希),所以没有被hack。

晚上

成功调节了AI,现在AI的回复速度更快,能够读取链接且上架了豆包!
不过说话风格和代码风格离我稍微远去了。

Day 2

早上

早上倒是没有什么特殊的,不过今天有一件大事:THUPC!

上午

Manacher(重点)

在我的AI复制智能体的帮助下,我学会了 Manacher 算法:

  1. 在所有字符串的两侧插入特殊字符'#',统一所有回文串的奇偶性,并且在字符串的两端加上不相同的哨兵'$'和'@'。

  2. 维护 \(C\)(最右回文串的中心)和 \(R\)(最右回文串的右边界)以及数组 \(p\) 满足
    \(\forall p_i \in p, p_i\)\(i\) 为中心的回文串的长度。

  3. \(\forall i \in [2, n - 1]\)

  • 如果 \(i\) 还在 \(R\) 内,那就用 \(p_{2C - i}\) 缩小 \(p\) 的范围,防止重复计算了 \(p\)

  • 暴力扩展 \(p_i\) 的长度。

  • 通过判断来修改更新 \(C\)\(R\)
    同样因为 \(i\) 指针是只进不退的,所以我们可以记为他是 \(\mathcal{O}(n)\) 的。

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxn = 1.1e7 * 2 + 5;
int p[maxn];
string preprocess(string s) {
string res = "$#";
for (char c : s) {
res += c;
res += '#';
}
res += "@";
return res;
}
int Manacher(string s) {
s = preprocess(s);
int n = s.size();
int C = 0, R = 0;
int maxLen = 0;
for (int i = 1; i < n - 1; i++) {
p[i] = (R > i) ? min(R - i, p[2 * C - i]) : 1;

while (s[i + p[i]] == s[i - p[i]])
   p[i]++;

if (i + p[i] > R) {
   R = i + p[i];
   C = i;
}

maxLen = max(maxLen, p[i] - 1);
}
return maxLen;
}
string s;
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);

cin >> s;
cout << Manacher(s) << endl;

return 0;
}

常数问题!!!

20a05747c2b91e1a4ed70283a922f696
AI还是太强了,一眼丁真发现问题!
接下来,五遍模板!
image
image
image
image
image

大事件:THUPC

我们吃完饭开始打THUPC,首先冲的是签到题 M,然而网卡了:
07ae603faee945eff689d95e0e36ac15
吃了7分罚时。

M

image
image
image
这道题的重点在于:
image
说明本题应该用谐音理解。那么易得遭成乃造成。

J

吃了两法罚时,别问罚时谁贡献的。
注意到LYD成功推出了J题的正解

#include<bits/stdc++.h>
#define int long long
using namespace std;
vector<int> p;
int t,n,m;
void init()
{
p.push_back(1);
for(int i=1;i<=62;i++)    p.push_back(p[p.size()-1]*2);
//    for(int i=0;i<p.size();i++)    cout<<p[i]<<"\n";
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
init();
cin>>t;
while(t--)
{
cin>>n>>m;
if(n%2==0)    cout<<"No\n";
else
{
   int cnt=0;
   while(!binary_search(p.begin(),p.end(),m))
   {
	   int x=*upper_bound(p.begin(),p.end(),m);
	   m=x-m,cnt++;
   }
   if((lower_bound(p.begin(),p.end(),m)-p.begin())%2==1)    cout<<"No\n";
   else if(2*cnt+1<=n)    cout<<"Yes\n";
   else    cout<<"No\n";
}
}
}

首先很容易就可以证明:当 \(n\) 为偶数的时候一定会挂。
\(n\) 为奇数,我们就可以去不断的查找 \(m\) ,然后让 \(m\) 变为比 \(m\) 大 的第一个 \(2^k\) 的差,直到:

\[\log_2m = \lfloor \log_2m \rfloor \]

如果此时 \(\log_2m\) 为奇数,那么就不能
如果跳 \(m\) 的次数小于等于 \(n\),那就可以
反之则不行(假了)

G

注意到我们后几个小时都在冲这道题,可惜没冲出来(LYD的做法TLE了)
我贡献了一个暴力对拍,可是对拍拍不出来TLE!
这你扯不扯。

结局

Rank #623 ,铁

下午

在打麻将。开局和了两把然后一直在输,最终点炮被击飞。

THUPC(讲评)

G

每次将序列中的 \(1\) 提取出来,插入到 \(S_1\) 中,不断的分离 \(S_1\)\(S_0\)。最终让 \(\lvert S_1\rvert\)\(\lvert S_0\rvert\) 趋于相等就好了。

晚自习

状压DP(重中之重)

P3052

这是一道橙色的搜索题目,这里使用状压DP。
0-indexed
可以将一头奶牛 \(k\) 是否被运输记录为 \(2^k\),然后可以定义状态 \(f_{i, j}\) 表示第 \(i\) 辆电梯最少运输的质量,其中:

\[j = \sum_{k = 0}^{k \lt n} 2^k \]

初始化所有的状态为 \(+\infty\),并且存储奶牛为状态。
我们首先在第一辆电梯上放上一只奶牛,然后向下转移:

\[\forall k \in [0, n), f_{i, j} \xrightarrow{+ c_k} \begin {aligned} \begin {cases} f_{i, j \oplus 2^k}, & f_{i, j} + c_k \le w \land f_{i, j} + c_k \lt f_{i, j \oplus 2^k}\\ f_{i, j \oplus 2^k}, & f_{i, j} + c_k \gt w \land f_{i, j} + c_k \lt f_{i, j \oplus 2^k} \end {cases} \end {aligned} \]

最终的答案:

\[\min \{i \mid f_{i, 2^n - 1} \neq +\infty\} \]

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int n, w;
int c[20];
int f[20][1 << 20];
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);

for (int i = 1; i <= 18; i++)
for (int j = 1; j < (1 << 18); j++)
   f[i][j] = INT_MAX;

cin >> n >> w;
for (int i = 0; i < n; i++)
cin >> c[i], f[1][1 << i] = c[i];

for (int i = 1; i <= n; i++) {
for (int j = 1; j < ( 1 << n ); j++) {
   if (f[i][j] == INT_MAX)
	   continue;
   for (int k = 0; k < n; k++) {
	   if ( j & (1 << k) ) continue;
	   if ( f[i][j] + c[k] <= w ) {
		   f[i][j | (1 << k)] = min(f[i][j | (1 << k)], f[i][j] + c[k]);
	   } else {
		   f[i + 1][j | (1 << k)] = c[k];
	   }
   }
}
}
for (int i = 1; i <= n; i++)
if (f[i][ (1 << n) - 1] != INT_MAX)
   return cout << i << endl, 0;
return 0;
}

image

P2622

0-indexed
这道题是一道很标准的状压DP,我们可以将第 \(k\) 盏灯的开关记录为一种二进制状态( \(1\) 为开,\(0\) 为关),并且用 \(f_j\) 记录走到当前状态所需要的最小次数。
其中,

\[j = \sum_{k = 0}^{k \lt n} 2^k \]

怎么转移呢?我们可以把每个状态用题中所给的 \(a\) 数组进行图论建模,并通过 \(SPFA\) 算法求出最短路并取值。针对每一个状态有这样的状态转移方程( \(LaTeX\) 恐惧症预警)

\[\forall i \in [1, m], f_s \xrightarrow {+ 1} f_{g(s, i, n - 1)} \\ g(s, i, j) = \begin {aligned} \begin {cases} s, & j = 0 \\ g(s, i, j - 1) \& ({\sim {2 ^ j}}), & a_{i, j} = 1 \\ g(s, i, j - 1) \mid {2 ^ j}, & a_{i, j} = -1 \\ g(s, i, j - 1), & a_{i, j} = 0 \end {cases} \end {aligned} \]

看似这个方程很吓人,实则仔细理解就会发现这只是看着吓人而已:\(g(s, i, j)\) 的作用是修改 \(s\) 的位。
然后使用 \(SPFA\) 不断的去取 \(s\) 状态就好了。

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxm = 105;
int f[1 << 10 + 5];
int n, m;
bool vis[1 << 10 + 5];
int a[maxm][10];
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);

cin >> n >> m;
for (int i = 1; i <= m; i++)
for (int j = 0; j < n; j++)
   cin >> a[i][j];

for (int i = 0; i < (1 << n) - 1; i++)
f[i] = INT_MAX;
f[ (1 << n) - 1] = 0;
queue<int> que;
que.push( (1 << n) - 1);
while (!que.empty()) {
int stat = que.front();
que.pop();
vis[stat] = 0;
for (int i = 1; i <= m; i++) {
   bitset<12> trans = stat;
   for (int j = 0; j < n; j++) {
	   if (a[i][j] == 1) trans.set(j, 0);
	   if (a[i][j] == -1) trans.set(j, 1);
   }
   int to = trans.to_ullong();
   if (f[stat] + 1 < f[to]) {
	   f[to] = f[stat] + 1;
	   if (!vis[to]) {
		   vis[to] = 1;
		   que.push(to);
	   }
   }
}
}
if (f[0] != INT_MAX)
cout << f[0] << endl;
else
cout << -1 << endl;
return 0;
}

image

晚上

触电了!
洗完澡之后去烧水,结果触电了,然而接下来触发了一连串的匪夷所思的唐诗行为:

  1. 触发漏电保护器后还想烧水

  2. 用控制变量法发现插座没电后去把电闸推上去了

  3. 刚漏电就烧水

好的,我还活着。
被电了是好彩头啊!
似乎也没有什么东西要学,晚上就简单的配置一下电脑吧。

Day 3

早上

起晚了。结果发现到早了。

图论 & 网络流专题(全是重点)

双连通分量(Tarjan)

待学习

边双

若一个无向图中的去掉任意一条边都不会改变此图的连通性,即不存在桥,则称作边双连通图。一个无向图中的每一个极大边双连通子图称作此无向图的边双连通分量。
连接两个边双连通分量的边即是桥。

点双

若一个无向图中的去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称作点双连通图。一个无向图中的每一个极大点双连通子图称作此无向图的点双连通分量。
注意一个割点属于多个点双连通分量。

欧拉路径

有向图

首先我们需要通过这样的一些条件来判定一个图是否具有欧拉路径:

  1. 这个图是一个连通图

  2. \(g(x) \lor h(x)\)

  • 如果存在欧拉回路则需要满足 \(g(x)\)
    其中:

  • \(g(x)\):每个节点的 \(inDegree = outDegree\)

  • \(h(x)\):有一个节点的 \(inDegree = outDegree + 1\)(称之为初始节点 \(s\) ), 另一个节点的 \(inDegree = outDegree - 1\), 其余节点皆满足 \(inDegree = outDegree\)
    然后进行这样的 \(dfs(x)\)

  1. 遍历与 \(x\) 相连的边 \(e_i\) (至步骤 \(3\)

  2. 记录下 \(e_i\) 指向的点 \(y\),然后删除掉 \(e_i\)

  3. \(dfs(y)\)

  4. 在答案栈里 \(push\)\(x\)
    我们可以写出这样的代码:

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxn = 1e5+5;
struct edge{
int to;
bool deleted;
};
vector<edge> graph[maxn];
int in[maxn], out[maxn];
int n, m;
stack<int> stk;
#define ans stk
void dfs(int x) {
bool operated = 0;
for (int i = 0; i < graph[x].size(); i++) {
   auto elem = graph[x][i];
   if (elem.deleted) continue;
   graph[x][i].deleted = 1;
   dfs(elem.to);
}
stk.push(x);
}
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
   int u, v;
   cin >> u >> v;
   graph[u].push_back({v, 0});
   out[u]++, in[v]++;
}
int tot = 0;
int s = 0, t = 0;
for (int i = 1; i <= n; i++) {
   sort(graph[i].begin(), graph[i].end(), [](edge a, edge b) {
       return a.to < b.to;
   });
   if (in[i] != out[i]) {
       tot++;
       if (in[i] == out[i] - 1)
           s = i;
       if (out[i] == in[i] - 1)
           t = i;
   }
}
if (tot != 0 && tot != 2) {
   cout << "No" << endl;
   return 0;
}
if (!tot)
   s = t = 1;
if (s == 0 || t == 0) {
   cout << "No" << endl;
   return 0;
}

dfs(s);
while(!ans.empty()) {
   cout << ans.top() << ' ';
   ans.pop();
}
cout << endl;

return 0;
}

image
为什么会 \(TLE\) 呢?
在我们的代码中,我们可以注意到在节点被删除后还存在与图上,只是被标记了。这导致时间复杂度暴涨,那么我们可以用一个 \(multiset\) 来维护这个东西,自动排序 + \(\mathcal{O}(1)\) 的删除:

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxn = 1e5+5;
multiset<int> graph[maxn];
int in[maxn], out[maxn];
int n, m;
stack<int> stk;
#define ans stk
void dfs(int x) {
bool operated = 0;
while (!graph[x].empty()) {
   int elem = *graph[x].cbegin();
   graph[x].erase(graph[x].cbegin());
   dfs(elem);
}
stk.push(x);
}
signed main() {
cin.tie(0), cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
   int u, v;
   cin >> u >> v;
   graph[u].insert(v);
   out[u]++, in[v]++;
}
int tot = 0;
int s = 0, t = 0;
for (int i = 1; i <= n; i++) {
   if (in[i] != out[i]) {
       tot++;
       if (in[i] == out[i] - 1)
           s = i;
       if (out[i] == in[i] - 1)
           t = i;
   }
}
if (tot != 0 && tot != 2) {
   cout << "No" << endl;
   return 0;
}
if (!tot)
   s = t = 1;
if (s == 0 || t == 0) {
   cout << "No" << endl;
   return 0;
}

dfs(s);
while(!ans.empty()) {
   cout << ans.top() << ' ';
   ans.pop();
}
cout << endl;

return 0;
}

注意不要损坏迭代器。
image

无向图

与有向图类似,需要这样的两个条件:

  1. 这个图是一个连通图

  2. \(g(x) \lor h(x)\)

  • 如果存在欧拉回路则需要满足 \(g(x)\)
    其中:

  • \(g(x)\):每个节点的 \(Degree \bmod 2 = 0\)

  • \(h(x)\):有两个节点的 \(Degree \bmod 2 = 1\),其余节点皆满足 \(Degree \bmod 2 = 0\)
    注意:无向图删边要删两条边!

A 最小生成树

有一个完全图,边 \((i, j)\) 的边权是 \(a_{i + j}\),求最小生成树大小
\(n \le 2 \times 10 ^ 5\)
给定一张 n 个点的完全图,点的编号为 1∼n,对于两个点 i,j(i=j),它们之间的边权为 ai+j​。请你求解这张图的最小生成树,你只需要给出最小生成树的边权和即可。
我们可以用一些点 \((i, d, 0 \lor 1)\) 表示 \(i + 2^{d - 1}\) 有没有和 \(v\) 合并。

\[merge \{(l, d - 1, 0) \sim (r + 2 ^{d - 1}), (l + 2^{d - 1}, d - 1, 0) \sim (r + 2 ^ {d - 1, d- 1, 0})\} \]

B Meeting for Meals

\(n\) 个点的 \(m\) 边带权无向图,\(k\) 个人要去往 \(1\) 。设 \(mx\) 为所有人到 \(1\) 的最小时间,问对于每个 \(i\),在 \(mx\) 时间内所有人到 \(1\) 的前提下,第 \(i\) 个人最多能有多长时间是与其他人一起动的。
\(n \le 3 \times 10^5, m \le 10^6, k \leq n\)
枚举每一条边,每次只考虑离他最近的人就好了。
正整数加法有性质 \(u+v>u\) ,所以距离最近点一定与原点直接相连。

C Hidden Graph

交互题,你有一个 \(n\) 个点的图,满足任意导出子图都存在一个点度数不超过 \(k\),每次你可以询问一个集合,返回其中任意一条边或返回无。求出这个图的所有边。询问次数不超过 \(2nk + n\)
增量,每次求出之前图的一个 \(k + 1\) 染色,那么每次问一个独立集,就可以知道一条边,次数 \(n(k + 1) + nk\) 。(边数不超过 \(nk\)

染色

让这个图的每个点在 \(1 \sim k\) 之间,然后让任意一条边连接的点的点权不同。

  1. 将当前未着色的点按度数降序排列。

  2. 将第一个点染成一个未被使用的颜色。

  3. 顺次遍历接下来的点,若当前点和所有与第一个点颜色 相同 的点 不相邻,则将该点染成与第一个点相同的颜色。

  4. 如果还有未着色的点,那就重复步骤 \(1\)
    从这里开始就是纯记录学习的图论知识了,就不记录题了

二分图

待学习

圆方树

待学习

匹配

待学习
定义无向图 \(G = (V, E)\) 的一组匹配为一个没有公共端点的边集 \(M \in E\)
最大匹配即大小最大的匹配(\(\sum_iM_i\)),对于两侧点数相等的二分图,还定义完美匹配为满足大小为一侧点数的匹配(相当于最大匹配)。
对于带边权的匹配,还可以定义最大权匹配等。
可以使用增广路求解

增广路

待学习
增广路是一条匹配边与非匹配边交错的路径,且首尾均为非匹配边。
将匹配边和非匹配边取反即可将匹配大小 \(+ 1\)。显然找不到增广路是该匹配为最大匹配的必要条件,这也是充分条件。
可以使用网络流求解

网络流(重点)

因为是大重点,所以用了二级标题。
对于一个特殊的 有向\(G\) ,其每条边有容量 \(c\),且有一个源点 \(s\) 和一个汇点 \(t\)
可以定义其有源汇流,对于每条边有流量 \(f(u, v) \le c(u, v)\),且除了源点和汇点外的点满足流量平衡,即

\[\sum_v f(u, v) = \sum_v f(v, u) \]

源点的入流量为 \(0\),汇点的出流量为 \(0\),汇点的出流量等于源点入流量。
可以定义 有源汇最大流\(\sum_v f(s, v)\) 最大的流。

FF 算法(求最大流框架)

注意我们直接每次找到一条从 \(s\)\(t\) 的路径并直接 \(-1\) 是不对的。
对于有向图中的每条边 \((u, v, c)\),考虑加入一条反向边 \((v, u, 0)\),每次找到一条从 \(s\)\(t\) 的路径,并将其剩余容量 \(-1\),将其反向边剩余容量 \(+1\),直到不能找到为止,可以证明其正确性。

最大流与最小割

定义有向图的边割集 \(\{s, t\}\) 表示将图中一些边去掉使得 \(s\) 无法到达 \(t\)
最小割即大小最小的割集。
我们证明一个定理:最大流 \(f\) 等于最小割 \(\mid \mid s, t \mid \mid\)
首先 \(f \le \mid \mid s, t \mid \mid\),因为每单位流量就需要占有单位的最小割中边的边权
并且 \(f \ge \mid \mid s, t \mid \mid\),这可以通过 \(FF\) 算法的流程证明。
因此,

\[f = \mid \mid s, t \mid \mid \]

EK 算法 (FF的实现)

每次使用 \(bfs\) 进行增广,找到一条路径就流路径上剩余容量的最小值,注意正反边都要操作。
最短路非递减定理:增广后任一点 \(dis\) 不会减小。
由于最短路不超过 \(n\) ,一条边的流量被清空两次后 \(dis = dis + 2\),因此增广论数不超过 \(\mathcal{O}(nm)\),时间复杂度 \(\mathcal{O}(nm^2)\)

Dinic 算法(EK的优化)

\(EK\) 算法基础上加入当前弧优化和多路增广,使用 \(bfs\) 分层,每次走 \(dis_v = dis_u + 1\) 的边。
时间复杂度 \(\mathcal{O}(n ^ 2m)\)。还可以加入炸点优化进行卡常。
一些结论:

  • 对于单位容量网络,增广复杂度 \(\mathcal{O}(m)\),轮数 \(\mathcal{O}(\sqrt{m})\)

  • 对于单位网络(出度或入度不超过 \(1\)),轮数 \(\mathcal{O}(n)\)

  • 例如二分图匹配复杂度:\(\mathcal{O}(m\sqrt n)\)
    值域优化: 将每条边二进制拆分,从大到小加入每份 \(2^k\) 容量,每轮复杂度 \(\mathcal{O}(nm)\),总复杂度 \(\mathcal{O}(nm\log_2 c)\)

网络流与匹配相关结论与构造

  • \(S\) 割集(\(S\) 能到的点):\(dinic\)\(dis\) 数组中非 \(+\infty\)\(T\) 割集即剩余的点。

  • 最小割:\(S\) 割集与 \(T\) 割集。

  • 最大权闭合子图:给一张有向图 \(G\),选每个点有收益(有正负),\(G\) 中边 \((u, v)\) 表示选 \(u\) 必须选 \(v\),求最大化收益:

  • 考虑原图每条边连 \((u, v, +\infty)\) ,对一个点 \(u\),若 \(val_u \le 0\),则连 \((s, u, val_u)\),否则连 \((u, t, \sim val_u)\),答案即正权之和减最大流。

  • 证明考虑最小割,方案构造即选 \(S\) 割集中的点。

    二分图匹配相关结论与构造

    \[\text{最大独立集} = n + m - \text{最小点覆盖} = n + m - 最大匹配 \]

  • 最大独立集:\(S\) 割集的左部点和 \(T\) 割集的左部点。

  • 最小点覆盖:最大独立集的补集。

费用流

在网络流的基础上加上了费用 \(z\),也就是:\((x, y, c, z)\)
\(bfs\) 改为 \(Dijkstra / SPFA\) 算法。

Ex-Hall 定理

二分图的最大匹配为 \(n + min_S(N(S) - S)\),可以用最大流 = 最小割证明。

偏序集

对于一个图,

\[\text{最小链覆盖} = n - \text{最大匹配} \]

每条边 \(x \rightarrow y\) 改成左部点 \(x\) 指向右部点 \(y\),跑最大匹配的点,没匹配的点就是链头。
对于偏序集,即若 \(x \rightarrow y, y \rightarrow z\),那么有边 \(x \rightarrow z\),其

\[\text{最长反链} = \text{最大独立集} = \text{最小链覆盖} = n - \text{最大匹配} \]

……其实这块基本就是把讲义理下来了,还得到时候自己学。
时间:\(2025/12/15, 11:32\)
需要梳理的内容:双连通分量,强连通分量,二分图,圆方树,匹配,增广路,网络流。

下午

在安装 \(Komorebi\) 的时候把镜像源回退到了 \(jammy\) 导致 \(apt\) 认为整个系统都是可清理的自动安装产物导致 \(Linux\) 被卸载……

花了一个下午重装。

晚自习

网络流模板(并未理解)

copy 了一个网络流板子。

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxn = 205;
int n, m, s, t;
struct netStream {
    struct edge {
        int to, w, rev; // 目标节点、剩余容量、反向边在对应邻接表的下标
    };
    vector<edge> graph[maxn]; // 邻接表存图,graph[u]是u的所有出边
    int dep[maxn], cur[maxn]; // dep:层次图深度;cur:当前弧优化指针
    inline void addEdge(int u, int v, int w) { // 加正向边+反向边
        graph[u].push_back({v, w, (int)graph[v].size()});   // 正向边,反向边下标是v当前邻接表大小
        graph[v].push_back({u, 0, (int)graph[u].size()-1}); // 反向边(容量0),正向边下标是u刚加的那条的下标
    }
    bool bfs(int S, int T) { // 构建层次图,返回S能否到T
        memset(dep, -1, sizeof(dep)); // 初始化层次为-1(未访问)
        queue<int> q;
        dep[S] = 0; // 源点层次设为0
        q.push(S);
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (auto &e : graph[u]) { // 遍历u的出边
                if (e.w > 0 && dep[e.to] == -1) { // 边有剩余容量且目标点未分层
                    dep[e.to] = dep[u] + 1; // 层次+1
                    q.push(e.to);
                    if (e.to == T) return true; // 到汇点了,提前返回
                }
            }
        }
        return dep[T] != -1; // 汇点是否被分层(即是否可达)
    }
    int dfs(int u, int flow, int T) { // 在层次图里找阻塞流,返回本次增广流量
        if (u == T) return flow; // 到汇点,返回能推的流量
        for (int &i = cur[u]; i < graph[u].size(); i++) { // 当前弧优化:i是引用,后续从i接着遍历
            auto &e = graph[u][i];
            if (e.w > 0 && dep[e.to] == dep[u] + 1) { // 边有剩余容量且在层次图下一层
                int minFlow = dfs(e.to, min(flow, e.w), T); // 递归找增广,取最小剩余容量
                if (minFlow > 0) { // 找到可行增广
                    e.w -= minFlow; // 正向边容量减少
                    graph[e.to][e.rev].w += minFlow; // 反向边容量增加(回退用)
                    return minFlow; // 返回这次增广的流量
                }
            }
        }
        return 0; // 没找到增广,返回0
    }
    int dinic(int S, int T) { // 计算最大流
        int maxFlow = 0;
        while (bfs(S, T)) { // 只要能分层(源到汇有路径)
            memset(cur, 0, sizeof(cur)); // 初始化当前弧指针
            int flow;
            while ((flow = dfs(S, LONG_LONG_MAX, T)) > 0) { // 多次找增广路,直到不能增广
                maxFlow += flow; // 累加流量
            }
        }
        return maxFlow;
    }
};
netStream stream;
signed main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        stream.addEdge(u, v, w);
    }
    cout << stream.dinic(s, t);
    return 0;
}

H (P2891)

首先我们有这样的一个思路:

\[s \rightarrow f \rightarrow cow \rightarrow d \rightarrow t \]

然而这是不满足题意的,因为这样会有一只小馋猫贪吃。那么我们可以把每头牛拆成两部分,一个连接 \(f\) ,一个连接 \(d\) 。这样就可以限制一只牛只能有一个流量通过。

\[s \rightarrow f \rightarrow cowA \rightarrow cowB \rightarrow d \rightarrow t \]

注意一条边只能连一次,不要有重边。

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxn = 505;
struct netStream {
    struct edge {
        int to, w, rev; // 目标节点、剩余容量、反向边在对应邻接表的下标
    };
    vector<edge> graph[maxn]; // 邻接表存图,graph[u]是u的所有出边
    int dep[maxn], cur[maxn]; // dep:层次图深度;cur:当前弧优化指针
    inline void addEdge(int u, int v, int w) { // 加正向边+反向边
        graph[u].push_back({v, w, (int)graph[v].size()});   // 正向边,反向边下标是v当前邻接表大小
        graph[v].push_back({u, 0, (int)graph[u].size()-1}); // 反向边(容量0),正向边下标是u刚加的那条的下标
    }
    bool bfs(int S, int T) { // 构建层次图,返回S能否到T
        memset(dep, -1, sizeof(dep)); // 初始化层次为-1(未访问)
        queue<int> q;
        dep[S] = 0; // 源点层次设为0
        q.push(S);
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (auto &e : graph[u]) { // 遍历u的出边
                if (e.w > 0 && dep[e.to] == -1) { // 边有剩余容量且目标点未分层
                    dep[e.to] = dep[u] + 1; // 层次+1
                    q.push(e.to);
                    if (e.to == T) return true; // 到汇点了,提前返回
                }
            }
        }
        return dep[T] != -1; // 汇点是否被分层(即是否可达)
    }
    int dfs(int u, int flow, int T) { // 在层次图里找阻塞流,返回本次增广流量
        if (u == T) return flow; // 到汇点,返回能推的流量
        for (int &i = cur[u]; i < graph[u].size(); i++) { // 当前弧优化:i是引用,后续从i接着遍历
            auto &e = graph[u][i];
            if (e.w > 0 && dep[e.to] == dep[u] + 1) { // 边有剩余容量且在层次图下一层
                int minFlow = dfs(e.to, min(flow, e.w), T); // 递归找增广,取最小剩余容量
                if (minFlow > 0) { // 找到可行增广
                    e.w -= minFlow; // 正向边容量减少
                    graph[e.to][e.rev].w += minFlow; // 反向边容量增加(回退用)
                    return minFlow; // 返回这次增广的流量
                }
            }
        }
        return 0; // 没找到增广,返回0
    }
    int dinic(int S, int T) { // 计算最大流
        int maxFlow = 0;
        while (bfs(S, T)) { // 只要能分层(源到汇有路径)
            memset(cur, 0, sizeof(cur)); // 初始化当前弧指针
            int flow;
            while ((flow = dfs(S, LONG_LONG_MAX, T)) > 0) { // 多次找增广路,直到不能增广
                maxFlow += flow; // 累加流量
            }
        }
        return maxFlow;
    }
};
netStream stream;
int n, f, d;
// 映射图坐标与物品
#define food(i) (1 + i)
#define cowA(i) (1 + f + 2 * (i - 1) + 1)
#define cowB(i) (1 + f + 2 * i)
#define drink(i) (1 + f + 2 * n + i)
signed main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    cin >> n >> f >> d;
    int s = 1, t = 1 + f + 2 * n + d + 1;
    // 先连好图
    for (int i = 1; i <= f; i++)
        stream.addEdge(s, food(i), 1);
    for (int i = 1; i <= d; i++)
        stream.addEdge(drink(i), t, 1);
    for (int i = 1; i <= n; i++)
        stream.addEdge(cowA(i), cowB(i), 1);
    for (int i = 1; i <= n; i++) {
        int $f_i$, $d_i$; // 注意到这是LaTeX
        cin >> $f_i$ >> $d_i$;
        for (int j = 1; j <= $f_i$; j++) {
            int x;
            cin >> x;
            stream.addEdge(food(x), cowA(i), 1);
        }
        for (int j = 1; j <= $d_i$; j++) {
            int x;
            cin >> x;
            stream.addEdge(cowB(i), drink(x), 1);
        }
    }
    cout << stream.dinic(s, t);
    return 0;
}

image

J (P7368)

我们可以把每个点的 \(r\)\(c\) 连起来,如果其还能连通,那么就证明其仍然存在。当整个图都无法保证连通的时候就证明所有的石头都被摧毁了。所以这个图就转化成了 最小割 问题,而最小割的值与 最大流 相等。
仍然注意不要有重边。

#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
constexpr int maxn = 2e3+5;
bool connected[maxn][maxn];
struct netStream {
    struct edge {
        int to, w, rev; // 目标节点、剩余容量、反向边在对应邻接表的下标
    };
    vector<edge> graph[maxn]; // 邻接表存图,graph[u]是u的所有出边
    int dep[maxn], cur[maxn]; // dep:层次图深度;cur:当前弧优化指针
    inline void addEdge(int u, int v, int w) { // 加正向边+反向边
        if (connected[u][v]) return;
        connected[u][v] = 1;
        graph[u].push_back({v, w, (int)graph[v].size()});   // 正向边,反向边下标是v当前邻接表大小
        graph[v].push_back({u, 0, (int)graph[u].size()-1}); // 反向边(容量0),正向边下标是u刚加的那条的下标
    }
    bool bfs(int S, int T) { // 构建层次图,返回S能否到T
        memset(dep, -1, sizeof(dep)); // 初始化层次为-1(未访问)
        queue<int> q;
        dep[S] = 0; // 源点层次设为0
        q.push(S);
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (auto &e : graph[u]) { // 遍历u的出边
                if (e.w > 0 && dep[e.to] == -1) { // 边有剩余容量且目标点未分层
                    dep[e.to] = dep[u] + 1; // 层次+1
                    q.push(e.to);
                    if (e.to == T) return true; // 到汇点了,提前返回
                }
            }
        }
        return dep[T] != -1; // 汇点是否被分层(即是否可达)
    }
    int dfs(int u, int flow, int T) { // 在层次图里找阻塞流,返回本次增广流量
        if (u == T) return flow; // 到汇点,返回能推的流量
        for (int &i = cur[u]; i < graph[u].size(); i++) { // 当前弧优化:i是引用,后续从i接着遍历
            auto &e = graph[u][i];
            if (e.w > 0 && dep[e.to] == dep[u] + 1) { // 边有剩余容量且在层次图下一层
                int minFlow = dfs(e.to, min(flow, e.w), T); // 递归找增广,取最小剩余容量
                if (minFlow > 0) { // 找到可行增广
                    e.w -= minFlow; // 正向边容量减少
                    graph[e.to][e.rev].w += minFlow; // 反向边容量增加(回退用)
                    return minFlow; // 返回这次增广的流量
                }
            }
        }
        return 0; // 没找到增广,返回0
    }
    int dinic(int S, int T) { // 计算最大流
        int maxFlow = 0;
        while (bfs(S, T)) { // 只要能分层(源到汇有路径)
            memset(cur, 0, sizeof(cur)); // 初始化当前弧指针
            int flow;
            while ((flow = dfs(S, LONG_LONG_MAX, T)) > 0) { // 多次找增广路,直到不能增广
                maxFlow += flow; // 累加流量
            }
        }
        return maxFlow;
    }
};
netStream stream;
int n, k;
signed main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    cin >> n >> k;
    int s = 1, t = 1 + 2 * n + 1;
    for (int i = 1; i <= k; i++) {
        int r, c;
        cin >> r >> c;
        r += 1;
        c += 1 + n;
        stream.addEdge(s, r, 1);
        stream.addEdge(r, c, 1);
        stream.addEdge(c, t, 1);
    }
    cout << stream.dinic(s, t);
    return 0;
}

晚上

使用 \(MarkText\) 编辑日记的时候导致了日记格式的损坏,花了 \(1.5h\) 修复一半。

将就看吧。

Day 4

上午

上午举行了一场模拟赛测试。

T1 query

有这样的三个长度为 \(n\) 的数组 \(a, b, c\)

给定一个正整数 \(k\),在所有 \(n^2\) 个二元组 \((i, j)~~(1 \le i \le n, 1 \le j \le n)\) 中, \(a_j + b_j \times c_j\) 的第 \(k\) 小值。

首先先打了个 \(32pts\) 的暴力。

然后在瞪性质的时候突然注意到了如果把 \(a\) 生成的序列排成一行的话,那么上下两个数字之间是有关系的:

\[\Delta_{i, j} = b_j ~\Delta a_i \]

换而言之,他们的差是 \(b\)\(a\) 的差的乘积。

然后我突然想到了 门捷列夫的元素周期表

接下来卡了 \(3.5h\) 因为初见思路是一个 \(CH_4\) 但是我看起来很像 \(Ac\) 的思路。

T2

暴力都不会,\(0pts\)

赛后发现是黑题,释怀了。

T3

题目中没有给出矩阵行列式的定义,不可做 T_T

T4

有这样的一个字符串 \(s\),要统计满足如下条件的有序非空字符串对 \((a, b)\) 的数量:

  • \(a, b\) 均为 \(s\) 的字串

  • 存在一个可以为空的字符串 \(c\),使得 \(a\)\(s\)每一次 出现后都会 紧接着 出现 \(cb\)\(b\)\(s\)每一次 出现之前都会 紧接着 出现 \(ac\)

\(998244353\) 取模。

为什么要打粗呢?因为有这样的 \(10pts\)

  • Case #\(4\)\(10pts, s\) 仅由一种字符组成。

理解错题意导致没拿到。

这道题考场上没冲出暴力,考后知道正解是明天要学的 \(SAM\)

改题

T1

我们可以固定 \(j\) 使得这些数字化为一些 一次函数

\[f_j(c) = b_jc + a_j \]

那么我们就可以先对 \(c\) 排序,然后二分出第 \(k\) 大的值 \(S\) ,针对每一个 \(S\) 可以第二次二分,针对每一个 \(j\) 求出有多少个 \(i\) 满足 \([ a_j + b_j \times c \le S ]\) 。然后令这个 \(i\) 的个数与 \(S\) 比较即可,时间复杂度 \(\mathcal{O}(nlog_2^2n)\)

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

constexpr int maxn = 1e5+5;

struct node{
    int a, b;
};

int n, k;
int base[maxn];
node elem[maxn];

bool check(int x) {
    int sum = 0;
    for (int j = 1; j <= n && sum < k; j++)
        sum += upper_bound(base + 1, base + 1 + n, (x - elem[j].a) / elem[j].b) - 1 - base;
    return sum < k;
}

signed main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);

    // freopen("query.in", "r", stdin);
    // freopen("query.out", "w", stdout);

    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> elem[i].a;
    for (int i = 1; i <= n; i++)
        cin >> elem[i].b;
    for (int i = 1; i <= n; i++)
        cin >> base[i];
    cin >> k;    

    sort(base + 1, base + 1 + n);


    int l = 1, r = 1e18 + 1e9;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (check(mid))
            l = mid + 1;
        else
            r = mid - 1;
    }
    cout << l << endl;
    return 0;
}

下午

强连通分量:缩点(重点)

注意到我昨天没学会这个东西,今天

\[\Large \textcolor{purple} 我 \textcolor{red} 悟 \textcolor{green} 了 \textcolor{blue} ! \]

缩点分为两部分,求强连通分量和缩图为点。

Tarjan 强连通分量

\(Tarjan\) 算法需要维护两个数组:\(dfn\)\(low\)\(dfn\) 表示这个节点是第几个进入 \(dfs\) 序的,\(low\) 表示这个节点最远能追溯到哪个节点。

首先我们需要这个东西来处理 \(dfn\)\(low\)

dfn[u] = low[u] = ++idx;
stk.push(u); // 入栈,仍在处理
vis[u] = 1; // 标记

for (int v : graph[u]) {
    if (!dfn[v]) { // 还没有被 dfs
        Tarjan(v);
        low[u] = min(low[u], low[v]);
    } else if (vis[v]){ // 已经被 dfs 且还在栈中
        low[u] = min(low[u], dfn[v]);
    }
}

当我们找到一个节点 \(u \in G\) 使得 \(dfn_u = low_u\) 的时候,我们就找到了一个强连通分量的根节点。这时候我们需要去不断的将栈顶的内容弹出并压入找到的 \(SCC\) (强连通分量)直到这个节点被弹出。

if (dfn[u] == low[u]) {
    sccCnt++; // 新建强连通分量
    while (!stk.empty()) {
        int x = stk.top();
        stk.pop(), vis[x] = 0;
        scc[x] = sccCnt, sccSize[x] += w[x]; // 压入强连通分量
        if (x == u)
            break;
    }
}

值得注意的是,\(Tarjan\) 算法本质上是 \(df\)s 且只能找到一个 \(SCC\) 。换句话说,想找到所有的 \(SCC\) 应当对每个未被遍历到的节点进行一次 \(Tarjan\)

for (int i = 1; i <= n; i++)
    if (!dfn[i]) Tarjan[i];

缩子图为点

现在我们需要把这个图转化为 \(DAG\) ,那就必须缩子图为点。

很简单了,只需要给每个点与其周边的非同一 \(SCC\) 的点所在的 \(SCC\) 建边就好了。

for (int u = 1; u <= n; u++)
    for (int v : graph[u])
        if (scc[u] != scc[v]) {
            dag[scc[u]].push_back(scc[v]);
            inDegree[scc[v]]++; // 用于DP
        }

后面只需要跑一个简单的树形 \(DP\) 就好了。

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

constexpr int maxn = 1e4 + 5, maxm = 1e5 + 5;

int n, m;
vector<int> graph[maxn];
int dfn[maxn], low[maxn], idx;
stack<int> stk;
bool vis[maxn];
int scc[maxn], sccSize[maxn], sccCnt;
int w[maxn];
vector<int> dag[maxn];
int inDegree[maxn];
int f[maxn];

void Tarjan(int u) {
	dfn[u] = low[u] = ++idx;
	stk.push(u);
	vis[u] = true;
	
	for (int v : graph[u]) {
		if (!dfn[v]) {
			Tarjan(v);
			low[u] = min(low[u], low[v]);
		} else if (vis[v])
			low[u] = min(low[u], dfn[v]);
	}
	if (dfn[u] == low[u]) {
		sccCnt++;
		while (!stk.empty()) {
			int x = stk.top();
			stk.pop();
			vis[x] = 0, scc[x] = sccCnt;
			sccSize[scc[x]] += w[x];
			if (x == u) break;
		}
	}
}

signed main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	cin >> n >> m;
	
	for (int i = 1; i <= n; i++)
		cin >> w[i];
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
	}
	
	for (int i = 1; i <= n; i++)
		if (!dfn[i]) Tarjan(i);
		
	for (int u = 1; u <= n; u++)
		for (int v : graph[u]) {
			if (scc[u] != scc[v]) {
				dag[scc[u]].push_back(scc[v]);
				inDegree[scc[v]]++;
			}
		}
		
	queue<int> que;
	for (int u = 1; u <= sccCnt; u++) {
		f[u] = sccSize[u];
		if (!inDegree[u])
			que.push(u);
	}
	
	while (!que.empty()) {
		int u = que.front();
		que.pop();	
		for (int v : dag[u]) {
			f[v] = max(f[v], f[u] + sccSize[v]);
			inDegree[v]--;
			if (!inDegree[v])
				que.push(v);
		}
	}	
	int ans = 0;
	for (int i = 1; i <= sccCnt; i++)
		ans = max(ans, f[i]);
	cout << ans << endl;
	return 0;
}

image

多刷几遍:

image
image

好!

割点

同样是 \(Tarjan\) ,但是判定的条件有所改变:

  • \(u\) 不是根节点,那么 \(low_v \ge dfn_u\) 的时候则这个点是割点
  • \(u\) 是根节点且其有至少两个儿子,那么其是根结点。
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;

constexpr int maxn = 2e4+5;
vector<int> graph[maxn];

int dfn[maxn], low[maxn], tot = 0;
set<int> cut;
void Tarjan(int u, int from) {
	dfn[u] = low[u] = ++tot;
	
	int rootSon = 0;
	
	for (int v : graph[u]) {
		if (!dfn[v]) {
			if (u == from) rootSon++;
			Tarjan(v, u);
			low[u] = min(low[u], low[v]);
			if (u != from && low[v] >= dfn[u])
				cut.insert(u);
		} else if (v != from)
			low[u] = min(low[u], dfn[v]);
	}
	if (rootSon >= 2)
		cut.insert(u);
}

int n, m;

signed main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
		graph[v].push_back(u);
	}
	
	for (int i = 1; i <= n; i++)
		Tarjan(i, i);
	
	cout << cut.size() << endl;
	for (int elem : cut)
		cout << elem << ' ';
	cout << endl;
	
	return 0;
}

image

Day 5

上午

今天的课是字符串,默认所有人都会:

\[KMP, Trie, Manacher, ACAM, SAM, PAM \]

就会前三,跑了。一个字听不懂。
接下来还是接着学图论:

点双连通分量

还是使用 \(Tarjan\) 算法,在每次判定一个节点的时候,如果遇到了割点,那么就把他从栈一直弹到割点的子节点(因为割点可以在很多个 点双 里)
值得注意的是,根节点无论如何都会和一个 点双 连通。最后不要忘记加上单独的点:

if (from == 0 && child == 0) bcc.push_back({u});

接下来就是 AC 代码

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

constexpr int maxn = 5e5+5;
vector<int> graph[maxn];

int dfn[maxn], low[maxn], idx;
vector<vector<int>> bcc;
stack<int> stk;

void Tarjan(int u, int from) {
    dfn[u] = low[u] = ++idx;
    stk.push(u);
    int child = 0;

    for (int v : graph[u]) {
        if (!dfn[v]) {
            if (from == 0) child++;
            Tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                vector<int> tmp;
                while (true) {
					int x = stk.top();
					stk.pop();
					tmp.push_back(x);
					if (x == v) break;
				}
                tmp.push_back(u);
                bcc.push_back(tmp);
            }
        } else if (v != from)
            low[u] = min(low[u], dfn[v]);
    }
    if (from == 0 && child == 0) bcc.push_back({u});
}

int n, m;

signed main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    for (int i = 1; i <= n; i++) {
        if (!dfn[i])
            Tarjan(i, 0);
    }
    
    cout << bcc.size() << endl;
    for (auto tmp : bcc) {
        cout << tmp.size() << ' ';
        for (int elem : tmp)
            cout << elem << ' ';
        cout << endl;
    }

    return 0;
}

注意到测试点太多贴不了截图。

边双连通分量

同点双,求出割边然后删掉这条边后输出所有的连通分量。
注意到割边与割点有所不同:

\[low_v > dfn_u \]

才能保证是割边(桥)。

注意到边双会被重边影响,所以单纯的 \(tag\) 是不可行的 —— 我使用了 \(multiset\) 来记录每一条边,而后删去割边即可。

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 5e5+5;

int n, m;
vector<int> graph[maxn];
multiset<int> bridge[maxn];

int dfn[maxn], low[maxn], idx;

void Tarjan(int u, int from) {
	dfn[u] = low[u] = ++idx;
	for (int v : graph[u]) {
		if (!dfn[v]) {
			Tarjan(v, u);
			low[u] = min(low[u], low[v]);
			if (low[v] > dfn[u]) {
				bridge[u].erase(lower_bound(bridge[u].begin(), bridge[u].end(), v));
				bridge[v].erase(lower_bound(bridge[v].begin(), bridge[v].end(), u));
			}
		} else if (v != from)
			low[u] = min(low[u], dfn[v]);
	}
}

bool vis[maxn];
int cnt = 0;
vector<int> ans[maxn];
void dfs(int u, int from) {
	ans[cnt].push_back(u);
	vis[u] = 1;
	for (int v : graph[u]) {
		if (v == from || vis[v] || !bridge[u].count(v)) continue;
		dfs(v, u);
	}
}

void main() {
	cin >> n >> m;
	
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
		graph[v].push_back(u);
		bridge[u].insert(v);
		bridge[v].insert(u);
	}
	for (int i = 1; i <= n; i++)
		if (!dfn[i])
			Tarjan(i, 0);
	for (int i = 1; i <= n; i++) {
		if (!vis[i]) {
			cnt++;
			dfs(i, 0);
		}
	}
	
	cout << cnt << endl;
	for (int i = 1; i <= cnt; i++) {
		cout << ans[i].size() << ' ';
		for (int elem : ans[i])
			cout << elem << ' ';
		cout << endl;
	}
	
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

注意到码风发生了

\[\Large 巨大改变! \]

晚自习

为了给我的 AI 智能体 Kibrel's Agent 喂数据,于是刷了一堆板子……
本来想学网络流的,看来只能明天找时间了。

Day 6

上午

切题最多的一集,四道。
构造还是太好做了,除了几道魔怔题不想做以外都切了。
贪心拼尽全力无法战胜,一道没切……

A

CF1408F Two Different

这道题很简单,找到一个 \(max_k\) 使得 \(2^k \le n\)
然后以 \(k\) 为中点开始二分处理整个序列就好了。

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 1e6+5;
int n, tot = 0, log[maxn];

struct node {
	int l, r;
} ans[maxn];

void dfs(int l, int r) {
	if (l == r)
		return;
	int mid = (l + r) >> 1;
	dfs(l, mid);
	dfs(mid + 1, r);
	for (int i = l; i <= mid; i++)
		ans[++tot] = {i, i - l + mid + 1};
}


void main() {
	log[1] = 1;
	for (int i = 2; i < maxn; i++)
		log[i] = log[i >> 1] << 1;
	cin >> n;
	dfs(1, log[n]);
	dfs(n - log[n] + 1, n);
	cout << tot << endl;
	for (int i = 1; i <= tot; i++)
		cout << ans[i].l << ' ' << ans[i].r << endl;
}

#undef int
#undef endl
	
}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

B

CF1172D Nauuo and Portals

考虑一个 \(n \times n\) 的问题如何转化成 \((n - 1) \times (n - 1)\):满足第一行和第一列。

如果已经满足直接缩小即可。

否则找到第一行中应该放在第一列那个和第一列中应该放在第一行那个,这两个位置各放一个传送门即可。

不断递归即可。

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

namespace OI {

#define int long long
#define endl "\n"

const int maxn = 1e6+5;

struct Portal {
	int x, y, p, q; // (x, y) -> (p, q)
} ans[maxn];

int tot = 0;
int n;

int a[maxn], b[maxn], c[maxn], d[maxn];
int ra[maxn], rb[maxn], rc[maxn], rd[maxn];

void main() {
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> b[i], rb[b[i]] = i;
	for (int i = 1; i <= n; i++)
		cin >> a[i], ra[a[i]] = i;
	for (int i = 1; i <= n; i++)
		c[i] = d[i] = rc[i] = rd[i] = i;

	for (int i = 1; i < n; i++) {
		if (c[i] == ra[i] && d[i] == rb[i])
			continue;
		ans[++tot] = {i, rc[ra[i]], rd[rb[i]], i};
		int t1 = c[i], t2 = d[i];
		swap(c[i], c[rc[ra[i]]]);
		swap(d[i], d[rd[rb[i]]]);
		swap(rc[ra[i]], rc[t1]);
		swap(rd[rb[i]], rd[t2]);
	}
	cout << tot << endl;
	for (int i = 1; i <= tot; i++)
		cout << ans[i].x << ' ' << ans[i].y << ' ' << ans[i].p  << ' ' << ans[i].q << endl;
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

C

CF Gym 102512 E Valentine

只需要分层构造就好了,我们需要构造若干个单调的数列并且按列分割,最后我们显而易见的可以得到这样的代码:

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 19900;

int T, x;
bitset<maxn + 1> f[205];

void dp(int i, int o) {
	int k = 200;
	while (k) {
		int j = 1;
		while (!f[k -j][i - j * (j - 1) / 2])
			j++;
		k -= j;
		i -= j * (j - 1) / 2;
		while (j--)
			cout << o++ << ' ';
		o--;
	}
	cout << endl;
}

void solve() {
	cin >> x;
	if (x < 200) {
		cout << "1 " << x << endl;
		for (int i = 0; i < x; i++)
			cout << 0 << " \n"[i == x - 1];
		return;
	}
	int n = 1;
	while (n * (n + 1) / 2 * 200 + n * 200 * 199 / 2 - 4949 < x)
		n++;
	cout << n << " 200\n";
	x -= n * (n + 1) / 2 * 200;
	for (int i = 0; i < n; i++) {
		int j = min(maxn, x);
		while (!f[200][j])
			j--;
		dp(j, i * 200);
		x -= j;
	}
}

void main() {
	cin >> T;
	
	f[0][0] = 1;
	for (int i = 1; i <= 200; i++)
		for (int j = 1; j <= i; j++)
			f[i] |= f[i - j] << j * (j - 1) / 2;
	
	while (T--)
		solve();
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

D、E、F

连着三道毒瘤题。

注意到 \(E\) 题使用了神秘小工具 \(Minecraft\) 进行教学,全场沸腾。

突然偶然有人误发三国杀司马昭技能:

QQ20251218-142237

于是:

wechat_2025-12-18_142340_324
wechat_2025-12-18_142400_898
wechat_2025-12-18_142410_013
wechat_2025-12-18_142418_674
wechat_2025-12-18_142427_762
wechat_2025-12-18_142438_725
wechat_2025-12-18_142447_992
wechat_2025-12-18_142559_559
wechat_2025-12-18_142605_245

我们的集训营正在蒸蒸日上哦!

G

AtCoder-arc183_d Keep Perfectly Matched

先求出重心,然后只需要向下 \(dfs\) 就好了。

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 2.5e5 + 5;
int n;
int son[maxn], root, parent[maxn], S;
vector<int> leaf[maxn], graph[maxn];

struct node {
	int a, b;
};

struct Compare{
	bool operator()(node a, node b) {
		return a.a < b.a;
	}
};

priority_queue<node, vector<node>, Compare> heap;

int weight[maxn], size[maxn];
void cent(int u, int from) {
	size[u] = 1;
	for (int v : graph[u]) {
		if (v == from)
			continue;
		cent(v, u);
		size[u] += size[v];
		weight[u] = max(weight[u], size[v]);
	}
	weight[u] = max(weight[u], n - size[u]);
	if (weight[u] <= n / 2)
		root = u;
		
}

void dfs(int u) {
	size[u] = 1;
	for (int v : graph[u]) {
		if (v == parent[u])
			continue;
		parent[v] = u;
		dfs(v);
		son[u]++;
		size[u] += size[v];
	}
}

void init(int u, int s) {
	leaf[s].push_back(u);
	if (size[u] & 1) {
		for (int v : graph[u]) {
			if (v == parent[u])
				continue;
			init(v, s);
		}
	} else {
		for (int v : graph[u]) {
			if (v == parent[u]) continue;
			if (size[v] + 1 & 1)
				init(v, s);
		}
		for (int v : graph[u]) {
			if (v == parent[u])
				continue;
			if (size[v] & 1)
				init(v, s);
		}
	}
}

void main() {
	cin >> n;
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
		graph[v].push_back(u);
	}
	cent(1, 0);
	for (int v : graph[root]) {
		parent[v] = root;
		dfs(v);
		init(v, v);
		if (size[v] & 1)
			S = v;
		else
			heap.push({leaf[v].size(), v});
	}
	size[root] = n;
	while (!heap.empty()) {
		int u = heap.top().b;
		heap.pop();
		int a = leaf[S].back(),
			b = leaf[u].back();
		leaf[S].pop_back();
		leaf[u].pop_back();
		cout << a << ' ' << b << endl;
		if (parent[a] != root)
			son[parent[a]]--;
		if (parent[b] != root)
			son[parent[b]]--;
		if (leaf[S].size())
			heap.push({leaf[S].size(), S});
		S = u;
	}
	cout << root << ' ' << S << endl;
}


#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

贪心笔记:调整法

在贪心中不存在两个数组没选完,必然有一个选完 / 不选。

可以把两个单调数列首(尾)进行比较,然后按照他们的优劣程度把这两个数列决定选完 / 不选。

最终得到至多一个数组被选择,但不一定要选完:
然后开始进行处理即可。

人话:随机化,不断的跳,把不符合大小条件的互换,直到可以过为止。

下午 & 晚上

和昨天一样,一直在刷以前学过的所有算法的板子。
发现一些曾经的算法(如单调栈和单调队列)已经开始忘记了。

\(QQ\) 群里的神秘消息:

wechat_2025-12-18_152021_598
wechat_2025-12-18_152037_872
wechat_2025-12-18_153807_533

修复了:遗忘 “单调栈” 和 “单调队列” 的 bug 。

Day 7

今天是休息日,上午:

打麻将

最终和了挺多的,注意到 ZYQ 收获了传奇挂件 YYY 得到了 “必胜” \(buff\) , 最终被我破解点炮两次。

中午:

\(Minecraft\)

玩了一会生存,大家都觉得没意思就下了。
最后我上 \(Hypixel\) 玩了一会起床战争。

下午:

你画我猜接龙

无需多言,最有意思的项目,请看 \(VCR\)

5a31fcb429046c10d52d7216a1f13f6a
d5dddb1209973a8c562e70d24a0a760b
d76844782f30303ddf04d7ce97e4a48d
e99d94b024ffd7dac0157096f8f4e52a
4f4cd504cd77e57a1f3c665819b916a1
a0cf1b1ac83f25c5fc7883dd52922a98
4aa94a038b50c2de2a68f4ba5ed8451a
d889ed313f4bf6595fc15693437b6c43
59ec60a3b8590c255d87b98405dadfbd
7c7f1546e0c554afd109bf391f4157a2
0cae3cd6b619f7b3e5df5e3b31e50ed1
12c01c981a7669704b637d93a49fbe4e
71a365a76e699c5c2a5158f006f4c10d
8234d7f909e040989621f9ed105d86af

晚上

后来玩累了。

开始犯偏头痛,最后就回房洗澡了。

本来计划要玩的很多,最后好像实现了,又好像没实现。

这是我所期待的吗?这不是我所期待的。
这是我所期待的吗?这是我所期待的。

可惜的是,似乎没有机会再和高二的两位同学这样耍一天了。

Day 8

动态规划!
大脑开始烧烤……

今天的题有点跨越时间,就不严格按时间做标题了

A

CF1810G

期望

期望的求法:

\(p_i\) 表示概率, \(x_i\) 表示值, 那么:

\[E = \sum_i x_ip_i \]

做法一

  • 如果只求一个 \(k\) 的答案要怎么做呢?

可以有 \(f_{i, j, k}\) 表示当前当前位置,当前前缀和,当前前缀和最大值
如果从后往前 \(dp\) ,设 \(f_{i, j}\) 表示填了 \(i\) 开头的后缀,这个后缀的最大值和为 \(j\) 的权值和。

然后可以转移到 $f_{i-1, max(0, j + a_{i - 1})}

对于单个 \(k\) , 复杂度 \(\mathcal{O}(n^2)\)

  • 背包问题状态怎么设?

\(f_{i, j}\) 表示考虑了前 \(i\) 个数字,和为 \(j\)
\(f_{i, j}\) 表示考虑了前 \(i\) 个数字,背包余量为 \(j\)

  • 返回刚才的想法

  • \(\textcolor{red} {从前往后}\) dp 。 设 \(f_{i, j}\) 表示填了 \(i-1\) 开头的前缀,钦定后面的最大前缀和为 \(j\)

\(f_{i - 1, max(0, j + a_{i - 1})}\) 转移来。

  • 把初值设定为 \(f_{0, S} = h_S\) 即可。

  • 注意:数组 \(a\) 不用管,直接按照 \(0\) 选择即可。

状态转移方程:

\[f_{i - 1, max(0, j + a_{i - 1})} \xrightarrow{\times p_{i - 1}} f_{i, j} \]

可以变得再明确些:

初始状态:

\[\forall i \in [0, n], f_{0, i} = h_i \]

转移方程:

\[f_{i, j} \leftarrow p_i \times f_{i - 1, j + 1} + (1 - p_i) \times f_{i - 1, max\{0, j - 1\}} \]

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int mod = 1e9+7, maxn = 5e3+5;

int T, n;

int fpow(int a, int b) {
	int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

int inv(int x) {
	return fpow(x, mod - 2);
}

int h[maxn], p[maxn];
int f[maxn][maxn];

void main() {
	cin >> T;
	while (T--) {
		cin >> n;
		for (int i = 0; i <= n + 1; i++)
			for (int j = 0; j <= n + 1; j++)
				f[i][j] = 0;
		for (int i = 1; i <= n; i++) {
			int x, y;
			cin >> x >> y;
			p[i] = x * inv(y) % mod;
		}
		for (int i = 0; i <= n; i++)
			cin >> h[i], f[0][i] = h[i];
		for (int i = 1; i <= n; i++)
			for (int j = 0; j <= n; j++)
				f[i][j] = ( p[i] * f[i - 1][j + 1] % mod + ( (1 - p[i]) * f[i - 1][max(0ll, j - 1)] + mod ) % mod + mod) % mod;
		for (int i = 1; i <= n; i++)
			cout << f[i][0] << ' ';
		cout << endl;
	}
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

做法二

  • 如果最大前缀和为 \(S\) , 那么一定存在的前缀和为 \(0 \dots S\) 的位置。

  • 对于每一个 \(x\) , 计算 \(S \ge x\) 的概率, 乘上 \(h_x - h_{x - 1}\) 求和。

设定 \(a\) 的前缀和 \(pre\) , 我们求:

存在一个 \(i\) 使得 \(pre_i = x\) 的方案的权值和。

  • 枚举 \(x\) , 再枚举集合 \(P\) , 钦定 \(P\) 中位置的 \(pre\) 等于 \(x\)

  • 这个方案有 \((-1)^{\mid P \mid - 1}\) 的权值。

  • 整个方案要乘上 \(h_x - h_{x - 1}\) 的权值。

  • 如果 \(x\) 固定怎么做?

\[\Large \textcolor{red} {我没听懂!} \]

B

AT_agc061_c

讲解

  • 既然每个区间都能选择左右断点,答案为什么不能是 \(2^n\)

  • 可能会有重复。

  • 思想:

    • 认为给一些方案加上限制,让某些方案变得不合法。
    • 目标:最后合法的答案与可能得到的 \(X\) 一一对应。
  • 给出一个 \(X\) , 如何判定它是否能够得到?

贪心,\(\mathcal{O}(n)\) 扫面所有端点,看看当前 \(X\) 的第一个数是否等于它,如果是,当前区间选择这个端点。

  • 得到了不存在满足下面条件的 \(i\)

  • \(i\) 的区间内部不包含我们选择的点,且 \(i\) 选择了右端点。

通过容斥原理枚举不合法区间,两个不合法区间会相交。
一个不合法区间可以确定所有与其相交的不合法区间,这些区间的下标是连续的。

我们需要图论建模一些边来做这道题,以形成 \(DAG\)

我的文字处理能力比较差,就直接把方程贴了:

初始化:

  • 定义一个图 \(spec\)
  • 定义对于数组的操作 \(\leftarrow\) 表示在数组的末尾加入数值
  • 状态数组 \(f_i\) 表示前 \(i\) 个的可能性

\[\Large \forall i \in [1, n], \\ \large \forall (k \mid A_{k + 1} \ge B{i}), (j \mid B_{j + 1} \ge A_i), \\ \normalsize spec[k] \leftarrow j \]

\[f_0 = 1 \]

状态转移方程:

\[f_i = f_{i - 1} \times 2 - \sum{j \in spec_i} f_j \]

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 5e5+5, mod = 998244353;

int n;
int A[maxn], B[maxn];

int f[maxn];
vector<int> spec[maxn];

void main() {
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> A[i] >> B[i];
	
	int j = 0, k = 0;
	for (int i = 1; i <= n; i++) {
		while (j < n && B[j + 1] < A[i])
			j++;
		while (k < n && A[k + 1] < B[i])
			k++;
		spec[k].push_back(j);
	}
	f[0] = 1;
	for (int i = 1; i <= n; i++) {
		int stat = 2 * f[i - 1] % mod;
		for (int j : spec[i])
			stat = (stat - f[j] + mod) % mod;
		f[i] = stat;
	}
	cout << f[n] << endl;
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

E

AT_abc290_h

结论

最后的权值序列是先增后减的。

  • 左边有 \(\lfloor \frac{n}{2} \rfloor\) 只狗和 \(\lfloor \frac{n}{2} \rfloor\) 只猫,权值不减。
  • 右边有 \(\lfloor \frac{n}{2} \rfloor\) 只狗和 \(\lfloor \frac{n}{2} \rfloor\) 只猫,权值不减。
  • 如果 \(n\) 为奇数,那么权值最大的狗放在中间;猫同理。

问题变成了这样:

有一个长度为 \(n + m\) 的不增序列,有红蓝两种颜色,红色有 \(n\) 个, 蓝色有 \(m\) 个。
要把这个序列分离成两个序列,各有 \(\frac{n}{2}\) 个红色和 \(\frac{m}{2}\) 个蓝色。
一个序列的权值定义为:每个位置上的数乘上它前面和它颜色不同的位置个数,求和。
要最小化两个序列的权值和。

可以设定 \(f_{i, j, k}\) 表示前 \(i\) 个数,第一个序列有 \(j\) 个红色和 \(k\) 个蓝色的最小代价。

因此让红色的前 \(\frac{n}{2}\) 和蓝色的后 \(\frac{m}{2}\) 放在第二个序列即可。

P.S. 从小到大排序

但是!

这道题是连续函数,完全可以用来练习模拟退火
粘了个板子然后研究了一下,打退火

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 305; // n+m 最大容量
int n, m;
int aArr[maxn], bArr[maxn];
int zt[maxn * 2], p[maxn * 2];
int bsa[maxn], bsb[maxn];
int ans = LONG_LONG_MAX; // 初始极大值

// 生成 [l, r] 随机数
int getRd(int l, int r) {
    return l + rand() % (r - l + 1);
}

// 计算当前状态的代价
int calc() {
    // 标记A/B类(0:A, 1:B)
    for (int i = 1; i <= n; i++) p[zt[i]] = 0;
    for (int i = n + 1; i <= n + m; i++) p[zt[i]] = 1;
    
    int cntA = 0, cntB = 0;
    int sumA = 0, sumB = 0;
    for (int i = 1; i <= n + m; i++) {
        if (p[i] == 0) { // A类逻辑
            bsa[++cntA] = abs(sumB - (m - sumB));
            sumA++;
        } else { // B类逻辑
            bsb[++cntB] = abs(sumA - (n - sumA));
            sumB++;
        }
    }
    
    // A类差异与aArr的贪心匹配
    int nowAns = 0;
    int nowL = 1, nowR = n;
    int cntT = 0;
    while (nowL <= nowR) {
        if (bsa[nowL] > bsa[nowR]) {
            nowAns += aArr[++cntT] * bsa[nowL++];
        } else {
            nowAns += aArr[++cntT] * bsa[nowR--];
        }
    }
    
    // B类差异与bArr的贪心匹配
    nowL = 1, nowR = m;
    cntT = 0;
    while (nowL <= nowR) {
        if (bsb[nowL] > bsb[nowR]) {
            nowAns += bArr[++cntT] * bsb[nowL++];
        } else {
            nowAns += bArr[++cntT] * bsb[nowR--];
        }
    }
    return nowAns;
}

// 模拟退火主流程
void sa() {
    double maxT = 1e6;   // 初始温度
    int nowAns = calc();
    ans = min(ans, nowAns);
    
    int cnt = 1;
    for (double nowT = maxT; nowT; nowT *= 0.9998) {
        // 每1024次检查时间是否超限
        if ((cnt & 1023) == 1023) {
            if (1.87 * CLOCKS_PER_SEC < clock()) break;
        }
        
        // 随机交换A、B类区域的位置
        int x = getRd(1, n);
        int y = getRd(n + 1, n + m);
        swap(zt[x], zt[y]);
        
        int delta = calc() - nowAns;
        // 拆掉Metropolis准则,直接比较
        if (delta < 0) {
            nowAns += delta;
        } else {
            swap(zt[x], zt[y]); // 回退无效交换
        }
        ans = min(ans, nowAns);
        cnt++;
    }
}

void main() {
    // Hopefully God will bless this code for AC
    // Amen
    // ✝️
    
    srand(time(0));
    
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> aArr[i];
    for (int i = 1; i <= m; i++) cin >> bArr[i];
    
    // a、b数组升序排序
    sort(aArr + 1, aArr + n + 1, [](int x, int y) {
		return x < y;
	});
    sort(bArr + 1, bArr + m + 1, [](int x, int y) {
		return x < y;
	});
    
    int tt = 50; // 多次初始化状态跑SA
    for (int i = 1; i <= tt; i++) {
        // 初始化zt为1~n+m,随机打乱
        for (int j = 1; j <= n + m; j++) zt[j] = j;
        random_shuffle(zt + 1, zt + n + m + 1);
        sa();
    }
    cout << ans << endl;
}

#undef int
#undef endl

}

int main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    OI::main();
    
    return 0;
}

对于这道题Metropolis准则没有丝毫帮助,直接拆掉。
退化为爬山算法之后居然 AC 了!
为什么呢?因为我们可以显而易见的证明这道题的函数图像是单峰函数。

待学习

单调队列优化,单调栈优化,斜率优化,WQS二分

随机化 —— 模拟退火学习

P2210

写了一个单次模拟退火随机化:

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 12 + 1; // 避讳那个不吉利的数字

// 随机化一定要用吉利的东西!!!

int n;
struct cow{
	int a, b, c;
} cows[maxn];

int pos[maxn];

int randint(int l, int r) {
	return l + rand() % (r - l + 1);
}

int calculate() {
	int ans = 0;
	for (int i = 1; i <= n; i++)
		ans += abs(pos[i] - pos[cows[i].a]),
		ans += abs(pos[i] - pos[cows[i].b]),
		ans += abs(pos[i] - pos[cows[i].c]);
	return ans;
}

void main() {
	// Hopefully God will bless this code for AC
	// Amen
	// ✝️
	
	srand(time(0));
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> cows[i].a >> cows[i].b >> cows[i].c;
	
	int cnt = 0, ans = INT_MAX;
	for (int i = 1; i <= n; i++)
		pos[i] = i;
	random_shuffle(pos + 1, pos + 1 + n);
	for (int temp = INT_MAX; temp; temp *= 0.998){
		if ( (cnt & 1023) == 1023 )
			if (0.97 * CLOCKS_PER_SEC < clock())
				break;
		int u = randint(1, n), v = randint(1, n);
		while (u == v)
			u = randint(1, n), v = randint(1, n);
		swap(pos[u], pos[v]);
		int nowAns = calculate();
		
		int delta = ans - nowAns;
		
		if (nowAns > ans && exp(-delta / temp) * RAND_MAX > rand())
			swap(pos[u], pos[v]);
			
		ans = min(ans, nowAns);
		
		cnt++;
	}
	cout << ans / 2 << endl;
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

P.S. 这道题可以用爬山算法得到 91pts

我学会了 \(SA\) !

网络流

今天自己写了一遍网络流的板子,并且已经熟练记忆网络最大流 \(dinic\) 算法:

// Author: Kibrel

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

namespace OI{

#define int long long
#define endl "\n"

constexpr int maxn = 205;

struct netFlow {
	struct edge {
		int to, w, rev;
	};
	vector<edge> graph[maxn];
	int depth[maxn], cur[maxn];
	
	inline void addEdge(int u, int v, int w) {
		graph[u].push_back({v, w, (int)graph[v].size()});
		graph[v].push_back({u, 0, (int)graph[u].size() - 1});
	}
	
	bool bfs(int S, int T) {
		for (int i = 0; i < (int)sizeof(depth) / sizeof(int); i++)
			depth[i] = -1;
		queue<int> q;
		depth[S] = 0;
		q.push(S);
		while (!q.empty()) {
			int u = q.front();
			q.pop();
			for (auto& e : graph[u]) {
				if (e.w && depth[e.to] == -1) {
					depth[e.to] = depth[u] + 1;
					q.push(e.to);
					if (e.to == T)
						return true;
				}
			}
		}
		return depth[T] != -1;
	}
	
	int dfs(int u, int flow, int T) {
		if (u == T)
			return flow;
		for (int &i = cur[u]; i < (int)graph[u].size(); i++) {
			auto &e = graph[u][i];
			if (e.w && depth[e.to] == depth[u] + 1) {
				int minFlow = dfs(e.to, min(flow, e.w), T);
				if (minFlow) {
					e.w -= minFlow;
					graph[e.to][e.rev].w += minFlow;
					return minFlow;
				}
			}	
		}
		return 0;
	}
	int dinic(int S, int T) {
		int maxFlow = 0;
		while (bfs(S, T)) {
			for (int i = 0; i < (int)sizeof(cur) / sizeof(int); i++)
				cur[i] = 0;
			int flow;
			while ( (flow = dfs(S, LONG_LONG_MAX, T)) ) {
				maxFlow += flow;
			}
		}
		return maxFlow;
	}
} flow;

int n, m, s, t;

void main() {
	cin >> n >> m >> s >> t;
	for (int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		flow.addEdge(u, v, w);
	}
	cout << flow.dinic(s, t) << endl;
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

Day 9

今天是省选测试。题非常的难,看见题目开头有神秘诗句立马吓哭。
后来只改会了一道题:

A1T1 (A2T3)

有三维绝对坐标序列 \(A\) 和三维相对坐标序列 \(B\) ,需要让他们一一对应使得他们的距离不会 变小

随机化好题,只需要用调整法开始打就好了。模拟退火其实也可以,只不过考场上 calc 函数写挂了。

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 505;

int n;
int x[maxn], y[maxn], z[maxn];
int u[maxn], v[maxn], w[maxn];
int a[maxn];

int dis(int x1, int y1, int z1, int x2, int y2, int z2) {
    return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2);
}

void main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> x[i] >> y[i] >> z[i];
    }
    for (int i = 1; i <= n; i++) {
        cin >> u[i] >> v[i] >> w[i];
    }
    srand(time(0));
    
    for (int i = 1; i <= n; i++)
		a[i] = i;
    random_shuffle(a + 1, a + 1 + n);
    
    while (1) {
        bool flag = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                if (dis(x[i], y[i], z[i], x[j], y[j], z[j]) > dis(x[i] + u[a[i]], y[i] + v[a[i]], z[i] + w[a[i]], x[j] + u[a[j]], y[j] + v[a[j]], z[j] + w[a[j]])) {
                    swap(a[i], a[j]);
                    flag = 0;
                }
            }
        }
        if (flag) {
            cout << "Yes" << endl;
            for (int i = 1; i <= n; i++) {
                cout << a[i] << " ";
            }
            return;
        }
    }
}

#undef int
#undef endl

}

int main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    OI::main();
    
    return 0;
}

下午

身体不舒服,睡了。

晚自习

晚上的时候把网络流题单里的题二刷了一遍。

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 1e3+5;

struct netFlow {
	struct edge {
		int v, w, rev;
	};
	vector<edge> graph[maxn];
	int depth[maxn], cur[maxn];
	
	void addEdge(int u, int v, int w) {
		graph[u].push_back({v, w, graph[v].size()});
		graph[v].push_back({u, 0, graph[u].size() - 1});
	}
	
	bool bfs(int S, int T) {
		memset(depth, -1, sizeof depth);
		
		queue<int> que;
		depth[S] = 0;
		que.push(S);
		while (!que.empty()) {
			int u = que.front();
			que.pop();
			for (auto& e : graph[u]) {
				if (e.w && depth[e.v] == -1) {
					depth[e.v] = depth[u] + 1;
					que.push(e.v);
					if (e.v == T)
						return 1;
				}
			}
		}
		return depth[T] != -1;
	}
	
	int dfs(int u, int flow, int T) {
		if (u == T)
			return flow;
		for (int &i = cur[u]; i < graph[u].size(); i++) {
			auto& e = graph[u][i];
			if (e.w && depth[e.v] == depth[u] + 1) {
				int minFlow = dfs(e.v, min(flow, e.w), T);
				if (minFlow) {
					e.w -= minFlow;
					graph[e.v][e.rev].w += minFlow;
					return minFlow;
				}
			}
		}
		return 0;
	}
	
	int dinic(int S, int T) {
		int maxFlow = 0;
		while (bfs(S, T)) {
			memset(cur, 0, sizeof cur);
			int flow;
			while ((flow = dfs(S, LONG_LONG_MAX, T))) {
				maxFlow += flow;
			}
		}
		return maxFlow;
	}
} flow;

#undef int
#undef endl	
	
int n, k;

#define row(i) 1 + i
#define col(i) 1 + n + i

void main() {
	cin >> n >> k;
	int S = 1, T = 1 + 2 * n + 1;
	for (int i = 1; i <= n; i++)
		flow.addEdge(S, row(i), 1), flow.addEdge(col(i), T, 1);
	for (int i = 1; i <= k; i++) {
		int r, c;
		cin >> r >> c;
		flow.addEdge(row(r), col(c), 1);
	}
	cout << flow.dinic(S, T) << endl;
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

Day 10

数据结构!

上午

上午就做出来一道题。

A

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 5e5 + 10;
const int inf = INT_MAX;

int n, m;
int a[maxn], mp[maxn], pre[maxn], pos[maxn];
int ans[maxn];

struct Query {
    int l, id;
};
vector<Query> requests[maxn];

struct SegNode {
    int left, right;
    int minVal;
} seg[maxn << 2];

void pushUp(int x) {
    seg[x].minVal = min(seg[x << 1].minVal, seg[x << 1 | 1].minVal);
}

void build(int x, int l, int r) {
    seg[x] = {l, r, inf};
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    pushUp(x);
}

void modify(int x, int pos, int k, int l, int r) {
    if (l == r) {
        seg[x].minVal = k;
        return;
    }
    int mid = (l + r) >> 1;
    if (pos <= mid) modify(x << 1, pos, k, l, mid);
    else modify(x << 1 | 1, pos, k, mid + 1, r);
    pushUp(x);
}

int query(int x, int L, int R, int l, int r) {
    if (L <= l && r <= R) return seg[x].minVal;
    int mid = (l + r) >> 1;
    int res = inf;
    if (L <= mid) res = min(res, query(x << 1, L, R, l, mid));
    if (R > mid) res = min(res, query(x << 1 | 1, L, R, mid + 1, r));
    return res;
}

void main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i], mp[i] = a[i];
    sort(mp + 1, mp + n + 1);
    int cnt = unique(mp + 1, mp + n + 1) - mp - 1;
    for (int i = 1; i <= n; i++) {
        a[i] = lower_bound(mp + 1, mp + cnt + 1, a[i]) - mp;
        pre[i] = pos[a[i]], pos[a[i]] = i;
    }
    for (int i = 1; i <= m; i++) {
        int l, r; cin >> l >> r;
        requests[r].push_back({l, i});
    }
    build(1, 1, n);
    for (int i = 1; i <= n; i++) {
        if (pre[i]) modify(1, pre[i], i - pre[i], 1, n);
        for (auto& req : requests[i]) {
            int val = query(1, req.l, i, 1, n);
            ans[req.id] = val == inf ? -1 : val;
        }
    }
    for (int i = 1; i <= m; i++)
		cout << ans[i] << endl;
}

#undef int
#undef endl

}

int main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    OI::main();
    
    return 0;
}

独立发明扫描线。

中午

身体不舒服,去睡觉了。下午两点半起,迟到了。

下午

\[\LARGE \mid x \mid \]

扫描线

首先我们需要把你要查找的区间变成一个图像。

image

如图所示,把整个矩形分成如图各个颜色不同的小矩形,小矩形的高是扫过的距离,然而矩形的水平宽一直在变化。

给每一个矩形的上下边进行标记,下面的边标记为 \(1\) ,上面的边标记为 \(-1\) 。每遇到一个水平边时,让这条边(在横轴投影区间)的权值加上这条边的标记。

可以说扫描线是一种思想,把要处理的东西画成图像,然后依次处理。

C

也就是一道扫描线题,

  1. 答案为 \(a_1 - l\) , 维护 \(l - r\) 中位置最小的 \(k\) 即可。
  2. 答案为 \(r - a_m\) , 维护 \(l - r\) 中位置最大的 \(k\) 即可。
  3. 答案为 \(a_i - a_{i - 1}\) ,在上一个位置记录子区间的答案即可。
// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 1e7 + 10;

int n, m, k;
int a[maxn];
int las[maxn];
int cnt, head[maxn];
struct edge {
    int to, nxt;
} e[maxn];
int t[maxn << 1];
int l[maxn], r[maxn], ans[maxn];

inline void update(int x, int y) {
    x += k;
    t[x] = y;
    for (x >>= 1; x; x >>= 1) {
        t[x] = max(t[x << 1], t[x << 1 | 1]);
    }
}

inline int query(int lRange, int rRange) {
    int res = -1e18;
    lRange += k - 1;
    rRange += k + 1;
    while (lRange ^ rRange ^ 1) {
        if (!(lRange & 1)) res = max(res, t[lRange ^ 1]);
        if (rRange & 1) res = max(res, t[rRange ^ 1]);
        lRange >>= 1;
        rRange >>= 1;
    }
    return res;
}

inline void fill() {
    memset(t, 0xcf, sizeof(t)); // 查询后发现需要初始化为 0xcf
    memset(las, 0, sizeof(las));
}

inline void add(int x, int y) {
    e[++cnt] = {y, head[x]};
    head[x] = cnt;
}

#define max(x, y) x = std::max(x, y)

void main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);

    cin >> n >> m;
    k = 1;
    while (k <= n + 1) k <<= 1;

    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= m; i++) {
        cin >> l[i] >> r[i];
        add(r[i], i);
    }

    fill();
    for (int i = 1; i <= n; i++) {
        if (las[a[i]]) {
            update(las[a[i]], i - las[a[i]] - 1);
        }
        las[a[i]] = i;
        for (int j = head[i]; j; j = e[j].nxt) {
            max(ans[e[j].to], query(l[e[j].to], r[e[j].to]));
        }
    }

    fill();
    for (int i = 1; i <= n; i++) {
        if (las[a[i]]) {
            update(las[a[i]], -1e18);
        }
        update(i, -i);
        las[a[i]] = i;
        for (int j = head[i]; j; j = e[j].nxt) {
            max(ans[e[j].to], i + query(l[e[j].to], r[e[j].to]));
        }
    }

    fill();
    memset(head, 0, sizeof(head));
    cnt = 0;
    for (int i = 1; i <= m; i++) {
        add(l[i], i);
    }
    for (int i = n; i >= 1; i--) {
        if (las[a[i]]) {
            update(las[a[i]], -1e18);
        }
        update(i, i);
        las[a[i]] = i;
        for (int j = head[i]; j; j = e[j].nxt) {
            max(ans[e[j].to], query(l[e[j].to], r[e[j].to]) - i);
        }
    }

    for (int i = 1; i <= m; i++) {
        cout << ans[i] << endl;
    }
}

#undef int
#undef endl

}

int main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    OI::main();
    
    return 0;
}

注意

应该练习线段树,按照我以前的写法,线段树是:

struct node {
	int left, right, content, tag;
};

这样常数非常大,所以现在应该练习新的线段树写法。

\(A\)\(C\) 题使用的搜索到的线段树模板。

晚自习

CF148D 概率 DP

和期望 \(DP\) 类似。
定义 \(f_{i, j}\) 表示选择 \(i\) 只白鼠和 \(j\) 只黑鼠。

初始化:

\[\forall i \in [1, w], f_{i, 0} = 1, f_{i, 1} = \frac{i}{i + 1} \]

状态转移方程:

\[f_{i, j} \leftarrow \frac{i}{i + j} + \frac{j}{i + j} \times \frac{j - 1}{i + j - 1} \times \frac{i}{i + j - 2} \times f_{i - 1, j - 2} + [ j \oplus 2 \neq 0 ] \times \frac{j}{i + j} \times \frac{j - 1}{i + j - 1} \times \frac{j - 2}{i + j - 2} \times f_{i, j - 3} \]

// Author: Kibrel

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

namespace OI {

#define int long long
#define float long double
#define endl "\n"

constexpr int maxn = 1e3+5;
int w, b;
float f[maxn][maxn];

void main() {
	cin >> w >> b;
	for (int i = 1; i <= w; i++)
		f[i][0] = 1.0, f[i][1] = 1.0 * i / (i + 1);
	for (int i = 1; i <= w; i++)
		for (int j = 2; j <= b; j++) {
			f[i][j] = 1.0 * i / (i + j) + 1.0 * j / (i + j) * (j - 1) / (i + j - 1) * i / (i + j - 2) * f[i - 1][j - 2];
			if (j ^ 2)
				f[i][j] += 1.0 * j / (i + j) * (j - 1) / (i + j - 1) * (j - 2) / (i + j - 2) * f[i][j - 3];
		}
	cout << fixed << setprecision(9) << f[w][b] << endl;
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

线段树

晚自习修改码风写了一份小常数线段树:

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 1e5+5;

struct node {
	int content, add;
} seg[maxn * 4];

int n, m;
int a[maxn];

void pushUp(int x) {
	seg[x].content = seg[2 * x].content + seg[2 * x + 1].content;
}

void build(int x, int L, int R) {
	if (L == R) {
		seg[x].content = a[L];
		return;
	}
	int mid = L + R >> 1;
	build(2 * x, L, mid);
	build(2 * x + 1, mid + 1, R);
	pushUp(x);
}

void tag(int x, int L, int R, int k) {
	seg[x].content += k * (R - L + 1);
	seg[x].add += k;
}

void pushDown(int x, int L, int R) {
	if (seg[x].add) {
		int mid = L + R >> 1;
		tag(2 * x, L, mid, seg[x].add);
		tag(2 * x + 1, mid + 1, R, seg[x].add);
		seg[x].add = 0;
	}
}

void add(int x, int L, int R, int l, int r, int k) {
	if (l <= L && R <= r) {
		tag(x, L, R, k);
		return;
	}
	pushDown(x, L, R);
	int mid = L + R >> 1;
	if (l <= mid)
		add(2 * x, L, mid, l, r, k);
	if (mid < r)
		add(2 * x + 1, mid + 1, R, l, r, k);
	pushUp(x);
}

int query(int x, int L, int R, int l, int r) {
	if (l <= L && R <= r) {
		return seg[x].content;
	}
	pushDown(x, L, R);
	int mid = L + R >> 1, ans = 0;
	if (l <= mid)
		ans += query(2 * x, L, mid, l, r);
	if (mid < r)
		ans += query(2 * x + 1, mid + 1, R, l, r);
	return ans;
}

void main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	build(1, 1, n);
	for (int i = 1; i <= m; i++) {
		int opt, x, y, k;
		cin >> opt >> x >> y;
		switch (opt) {
			case 1:
				cin >> k;
				add(1, 1, n, x, y, k);
				break;
			case 2:
				cout << query(1, 1, n, x, y) << endl;
				break;
		}
	}
}


#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();

	return 0;
}

Day 11

依旧是:

\[\LARGE \mid x \mid \]

的数据结构!

奇怪复杂度数据结构专题。

上午

A

这道题自己直接想出来了。

\(Y\) 比较大的时候,可以把整个集合分成块,然后按 \(Y\) 来分割去比较第一个数。
\(Y\) 比较小的时候,直接求就好了。

时间复杂度:\(\mathcal{O}(\sqrt{nlogn}n)\)

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int SQRT = 550, maxA = 3e5; // sqrt(3e5)
std::set<int> set;
int table[SQRT];

int n;

int query(int k) {
	if (k < SQRT)
		return table[k];
	int ans = INT_MAX;
	for (int i = 0; i * k <= maxA; i++) {
		int y = i * k;
		auto nowAns = set.lower_bound(y);
		if (nowAns == set.end())
			break;
		if (*nowAns < y + k)
			ans = min(*nowAns % k, ans);
	}
	return ans;
}

void main() {
	cin >> n;
	for (int i = 1; i < SQRT; i++)
		table[i] = INT_MAX;
	for (int i = 1; i <= n; i++) {
		char opt;
		int k;
		cin >> opt >> k;
		switch (opt) {
			case 'A':
				set.insert(k);
				for (int i = 1; i < SQRT; i++)
					table[i] = min(k % i, table[i]);
				break;
			case 'B':
				cout << query(k) << endl;
				break;
		}
	}
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

B

枚举中间的值,让 \(b = \frac{a_j}{a_i} = \frac{a_k}{a_j}\) , 易证 \(b \in [1, 1000]\)
我们可以暴力枚举 \(b\)

时间复杂度:\(\mathcal{O}(n^\frac{1}{3}n)\)

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 2e5+5;

int T, n;
int a[maxn], cnt = 0, maxA;
map<int, int> exist;

void main() {
	cin >> T;
	while (T--) {
		cnt = 0, maxA = 0;
		exist.clear();
		cin >> n;
		for (int i = 1; i <= n; i++)
			cin >> a[i], exist[a[i]]++, maxA = max(maxA, a[i]);
		sort(a + 1, a + 1 + n);
		for (int i = 1; i <= n; i++) {
			if (a[i] == a[i - 1])
				continue;
			int cur = exist[a[i]];
			if (cur >= 3)
				cnt += cur * (cur - 1) * (cur - 2);
			for (int j = 1; j <= 1000 && j * j <= a[i]; j++) {
				if (a[i] / j * j == a[i]) {
					if (j != 1) {
						if (exist.count(a[i] / j) && exist.count(a[i] * j)) {
							cnt += cur * exist[a[i] / j] * exist[a[i] * j];
						}
					}
					int k = a[i] / j;
					if (k != j) {
						if (exist.count(a[i] / k) && exist.count(a[i] * k)) {
							cnt += cur * exist[a[i] / k] * exist[a[i] * k];
						}
					}
				}
			}
		}
		cout << cnt << endl;
	}
}


#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();	
	
	return 0;
}

下午

L

陷阱题,用线段树维护最大值就好了。完全不用长链剖分。

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 2e5 + 2;

int n, T, leaf;
int dfn[maxn], ed[maxn], node[maxn], F[maxn], dep[maxn], w[maxn];
bool L[maxn], clear[maxn];
vector<pair<int, int>> graph[maxn];
int ans[maxn];

pair<int, int> mx[maxn << 2];
int tag[maxn << 2];

inline void apply(int x, int k) {
    mx[x].first += k;
    tag[x] += k;
}

inline void pushDown(int x) {
    if (tag[x]) {
        apply(x << 1, tag[x]);
        apply(x << 1 | 1, tag[x]);
        tag[x] = 0;
    }
}

inline void pushUp(int x) {
    mx[x] = max(mx[x << 1], mx[x << 1 | 1]);
}

inline void build(int x, int l, int r) {
    if (l == r) {
        mx[x] = {L[node[l]] ? 0 : -1e18, l};
        return;
    }
    int mid = (l + r) >> 1;
    build(x << 1, l, mid);
    build(x << 1 | 1, mid + 1, r);
    pushUp(x);
}

inline void update(int x, int l, int r, int L, int R, int k) {
    if (L <= l && r <= R) {
        apply(x, k);
        return;
    }
    pushDown(x);
    int mid = (l + r) >> 1;
    if (L <= mid) update(x << 1, l, mid, L, R, k);
    if (mid + 1 <= R) update(x << 1 | 1, mid + 1, r, L, R, k);
    pushUp(x);
}

inline pair<int, int> query(int x, int l, int r, int L, int R) {
    if (L <= l && r <= R) return mx[x];
    pushDown(x);
    int mid = (l + r) >> 1;
    pair<int, int> ans = {-1e18, 0};
    if (L <= mid) ans = max(ans, query(x << 1, l, mid, L, R));
    if (mid + 1 <= R) ans = max(ans, query(x << 1 | 1, mid + 1, r, L, R));
    return ans;
}

inline void dfs(int x, int fa) {
    L[x] = 1;
    dfn[x] = ed[x] = ++T;
    node[T] = x;
    F[x] = fa;
    dep[x] = dep[fa] + 1;
    for (auto pair : graph[x]) {
		auto t = pair.first;
		auto val = pair.second;
        if (t != fa) {
            L[x] = 0;
            dfs(t, x);
            ed[x] = ed[t];
        } else {
            w[x] = val;
        }
    }
    if (L[x]) leaf++;
}

int x[maxn], y[maxn], z[maxn];

void main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    cin >> n;
    for (int i = 1; i < n; i++) {
        cin >> x[i] >> y[i] >> z[i];
        graph[x[i]].push_back({y[i], z[i]});
        graph[y[i]].push_back({x[i], z[i]});
    }
    dfs(1, 0);
    
    build(1, 1, n);
    
    for (int i = 1; i < n; i++) {
        int u = x[i], v = y[i], w = z[i];
        if (dep[u] > dep[v]) swap(u, v);
        update(1, 1, n, dfn[v], ed[v], w);
    }
    
    for (int i = 1; i <= leaf; i++) {
        auto res = query(1, 1, n, 1, n);
        ans[i] = ans[i - 1] + res.first;
        
        int curNode = node[res.second];
        
        while (!clear[curNode] && curNode != 1) {
            update(1, 1, n, dfn[curNode], ed[curNode], -w[curNode]);
            clear[curNode] = 1;
            curNode = F[curNode];
        }
        cout << ans[i] * 2 << endl;
    }
    
    for (int i = leaf + 1; i <= n; i++) {
        cout << ans[leaf] * 2 << endl;
    }
}

#undef int
#undef endl

}

int main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    OI::main();
    
    return 0;
}

P4999 数位DP

// Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 1e4 + 5;
constexpr int mod = 1e9 + 7;

int T;
int l, r;
int a[20], numDigits;  // 数位拆分用(最多20位)
int f[20][maxn];       // 记忆化数组:f[当前位][已累加和]

int dfs(int pos, int sum, bool limit) {
	if (pos == 0)
		return sum % mod;
	if (!limit && f[pos][sum] != -1)
		return f[pos][sum];
	int upper = limit ? a[pos] : 9;
	int res = 0;
	for (int i = 0; i <= upper; i++)
		res = (res + dfs(pos - 1, sum + i, limit && (i == upper)) ) % mod;
	if (!limit)
		f[pos][sum] = res;
	return res;
}

int solve(int x) {
	numDigits = 0;
	while (x) {
		a[++numDigits] = x % 10;
		x /= 10;
	}
	return dfs(numDigits, 0, 1) % mod;
}

void main() {
    memset(f, -1, sizeof(f));
    
    cin >> T;
    while (T--) {
	    cin >> l >> r;
        int ans = (solve(r) - solve(l - 1) + mod) % mod;
        cout << ans << endl;
    }
}

#undef int
#undef endl

}

int main() {
    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);
    
    OI::main();
    
    return 0;
}

晚自习

https://www.luogu.com.cn/contest/165999

神秘炒股竞赛,允许 \(AI\) 介入。

Kibrel's Agent 获得了 \(879950\) 元的好成绩!
代码“稳定”到逆天,甚至删去一大段都不会出问题(?)

import sys
import math
import random
import time
from typing import Dict, List, Tuple, Optional, Any
from collections import defaultdict, deque

class Stock:
    """股票类,新增 `buy_price` 记录建仓成本"""
    def __init__(self, code: str):
        self.code = code
        self.price_history = []  # 收盘价历史
        self.volume_history = []  # 成交量历史
        self.turnover_history = []  # 换手率历史
        self.pct_chg_history = []  # 涨跌幅历史
        
        # 当日行情
        self.date = ""
        self.open = 0.0
        self.high = 0.0
        self.low = 0.0
        self.close = 0.0
        self.volume = 0
        self.amount = 0.0
        self.amplitude = 0.0
        self.pct_chg = 0.0
        self.change = 0.0
        self.turnover = 0.0
        
        # 持仓信息
        self.shares_held = 0  # 持有股数
        self.avg_cost = 0.0   # 平均成本
        self.total_invested = 0.0  # 总投入
        self.buy_price = 0.0  # 建仓时的买入价(计算利润率用)
        
        # 技术指标缓存(本次策略未使用,保留结构)
        self._ma5 = None
        self._ma10 = None
        self._ma20 = None
        self._rsi = None
        self._bollinger_upper = None
        self._bollinger_lower = None
        
    def update_data(self, data: Dict[str, Any]):
        """更新当日行情并记录历史"""
        self.date = data.get('date', self.date)
        self.open = float(data.get('open', self.open))
        self.high = float(data.get('high', self.high))
        self.low = float(data.get('low', self.low))
        self.close = float(data.get('close', self.close))
        self.volume = int(data.get('volume', self.volume))
        self.amount = float(data.get('amount', self.amount))
        self.amplitude = float(data.get('amplitude', self.amplitude))
        self.pct_chg = float(data.get('pct_chg', self.pct_chg))
        self.change = float(data.get('change', self.change))
        self.turnover = float(data.get('turnover', self.turnover))
        
        self.price_history.append(self.close)
        self.volume_history.append(self.volume)
        self.turnover_history.append(self.turnover)
        self.pct_chg_history.append(self.pct_chg)
        self._clear_cache()  # 清除技术指标缓存
    
    def _clear_cache(self):
        """清除技术指标缓存"""
        self._ma5 = self._ma10 = self._ma20 = None
        self._rsi = None
        self._bollinger_upper = self._bollinger_lower = None
    
    @property
    def is_suspended(self) -> bool:
        """判断是否停牌(简化:收盘价或成交量为0视为停牌)"""
        return self.close <= 0 or self.volume <= 0
    
    def update_position(self, shares: int, price: float, is_buy: bool):
        """更新持仓,首次买入时记录 `buy_price`"""
        if is_buy:
            total_shares = self.shares_held + shares
            total_cost = self.shares_held * self.avg_cost + shares * price
            self.shares_held = total_shares
            self.avg_cost = total_cost / total_shares if total_shares > 0 else 0.0
            self.total_invested += shares * price
            # 首次买入时记录建仓价
            if self.shares_held == shares:  
                self.buy_price = price
        else:
            self.shares_held -= shares
            if self.shares_held == 0:
                self.avg_cost = 0.0
                self.buy_price = 0.0  # 清仓后重置建仓价

class Portfolio:
    """投资组合管理,跟踪现金、持仓、总资产"""
    def __init__(self, initial_cash: float):
        self.initial_cash = initial_cash
        self.cash = initial_cash  # 可用现金
        self.positions: Dict[str, Stock] = {}  # 代码 -> Stock对象
        self.daily_values = []  # 每日总资产
        self.max_drawdown = 0.0  # 最大回撤
    
    @property
    def total_value(self) -> float:
        """总资产 = 现金 + 持仓市值"""
        position_value = sum(stock.shares_held * stock.close for stock in self.positions.values())
        return self.cash + position_value
    
    def update_daily_value(self):
        """更新每日总资产并计算最大回撤"""
        self.daily_values.append(self.total_value)
        if len(self.daily_values) > 1:
            peak = max(self.daily_values)
            trough = min(self.daily_values[-min(10, len(self.daily_values)):])
            drawdown = (peak - trough) / peak if peak > 0 else 0.0
            self.max_drawdown = max(self.max_drawdown, drawdown)

class TradingStrategy:
    """核心交易策略: $buy_cash_ratio$ 满仓买最便宜 + 概率卖出"""
    def __init__(self, portfolio: Portfolio, lot_size: int,
                 commission_rate: float, min_commission: float,
                 stamp_tax_rate: float, max_orders_per_day: int):
        self.portfolio = portfolio
        self.lot_size = lot_size        # 交易单位(手)
        self.commission_rate = commission_rate  # 佣金率
        self.min_commission = min_commission    # 最低佣金
        self.stamp_tax_rate = stamp_tax_rate    # 印花税率
        self.max_orders = max_orders_per_day    # 单日委托上限
        
        # 策略参数(固定)
        self.buy_cash_ratio = 1  # 满仓的 $buy_cash_ratio$ 用于买入
    
    def calculate_commission(self, amount: float) -> float:
        """计算佣金(取最低佣金或比例佣金的较大者)"""
        return max(amount * self.commission_rate, self.min_commission)
    
    def can_afford_buy(self, price: float, quantity: int) -> bool:
        """检查是否有足够现金买入(含佣金)"""
        amount = price * quantity
        commission = self.calculate_commission(amount)
        return self.portfolio.cash >= amount + commission
    
    def generate_buy_signals(self, all_stocks: Dict[str, Stock]) -> List[Tuple[int, str]]:
        """生成买入信号:用满仓 $buy_cash_ratio$ 买当前最便宜的股票"""
        buy_orders = []
        # 筛选:未持仓、未停牌、收盘价>0的股票
        candidates = [
            stock for code, stock in all_stocks.items()
            if stock.shares_held == 0 
            and not stock.is_suspended 
            and stock.close > 0
        ]
        if not candidates:
            return buy_orders
        
        # 按收盘价升序,取最便宜的
        candidates.sort(key=lambda s: s.close)
        target = candidates[0]
        total_value = self.portfolio.total_value
        available_cash = total_value * self.buy_cash_ratio  # 满仓的 $buy_cash_ratio$ 
        
        if available_cash <= 0:
            return buy_orders
        
        # 计算最大可买数量(整手,且覆盖佣金)
        price = target.close
        max_qty = int(available_cash / (price * (1 + self.commission_rate)))
        max_qty = (max_qty // self.lot_size) * self.lot_size  # 整手
        
        if max_qty < self.lot_size:
            return buy_orders  # 至少买1手
        
        # 精确检查现金是否足够(佣金可能取最低值)
        amount = price * max_qty
        commission = self.calculate_commission(amount)
        total_cost = amount + commission
        if self.portfolio.cash < total_cost:
            # 现金不足时,向下调整数量
            max_qty = int((self.portfolio.cash - commission) / price)
            max_qty = (max_qty // self.lot_size) * self.lot_size
            if max_qty < self.lot_size:
                return buy_orders
            amount = price * max_qty
            commission = self.calculate_commission(amount)
            total_cost = amount + commission
            if self.portfolio.cash < total_cost:
                return buy_orders  # 仍不足则放弃
        
        buy_orders.append( (max_qty, target.code) )
        return buy_orders
    
    def generate_sell_signals(self, stock: Stock) -> List[Tuple[str, int]]:
        """生成卖出信号:概率 = 1 / exp(2 - p/2),p=(当前价-买入价)/买入价"""
        sell_orders = []
        if stock.shares_held == 0 or stock.buy_price == 0:
            return sell_orders
        
        # 计算利润率 p
        p = (stock.close - stock.buy_price) / stock.buy_price
        # 计算卖出概率
        prob = 1 / math.exp(2 - p / 2)
        # 随机判定是否卖出
        if random.rand() < prob:
            # 卖出全部持仓(整手)
            sell_shares = (stock.shares_held // self.lot_size) * self.lot_size
            if sell_shares >= self.lot_size:
                sell_orders.append( ("SELL", sell_shares) )
        return sell_orders
    
    def generate_orders(self, all_stocks: Dict[str, Stock]) -> List[str]:
        """生成最终委托单:先卖后买,严格控制单量"""
        orders = []
        
        # 第一步:处理卖出信号
        for code, stock in list(self.portfolio.positions.items()):
            if stock.shares_held > 0:
                sell_signals = self.generate_sell_signals(stock)
                for _, shares in sell_signals:
                    sell_shares = (shares // self.lot_size) * self.lot_size
                    if sell_shares >= self.lot_size and sell_shares <= stock.shares_held:
                        orders.append(f"SELL {code} {sell_shares}")
                        if len(orders) >= self.max_orders:
                            return orders
        
        # 第二步:处理买入信号
        buy_signals = self.generate_buy_signals(all_stocks)
        for shares, code in buy_signals:
            stock = all_stocks[code]
            if self.can_afford_buy(stock.close, shares):
                orders.append(f"BUY {code} {shares}")
                if len(orders) >= self.max_orders:
                    return orders
        
        return orders[:self.max_orders]

class TradingSystem:
    """交易系统主流程:处理IO、调度策略"""
    def __init__(self):
        self.n = 0          # 股票数
        self.D = 0          # 交易天数
        self.lot_size = 100 # 交易单位(手)
        self.max_orders = 500  # 单日委托上限
        self.all_stocks: Dict[str, Stock] = {}  # 所有股票字典
        self.portfolio: Optional[Portfolio] = None  # 投资组合
        self.strategy: Optional[TradingStrategy] = None  # 交易策略
    
    def parse_init_line(self, line: str):
        """解析初始化参数行"""
        parts = line.strip().split()
        if parts[0] != "INIT":
            raise ValueError(f"无效初始化行: {line}")
        
        self.n = int(parts[1])
        self.D = int(parts[2])
        initial_cash = float(parts[3])
        self.lot_size = int(parts[4])
        commission_rate = float(parts[5])
        min_commission = float(parts[6])
        stamp_tax_rate = float(parts[7])
        self.max_orders = int(parts[8])
        
        # 初始化投资组合和策略
        self.portfolio = Portfolio(initial_cash)
        self.strategy = TradingStrategy(
            portfolio=self.portfolio,
            lot_size=self.lot_size,
            commission_rate=commission_rate,
            min_commission=min_commission,
            stamp_tax_rate=stamp_tax_rate,
            max_orders_per_day=self.max_orders
        )
    
    def parse_stock_line(self, line: str) -> Stock:
        """解析单只股票的行情行"""
        parts = line.strip().split(',')
        if len(parts) < 12:
            raise ValueError(f"无效行情行: {line}")
        
        code = parts[1]
        if code not in self.all_stocks:
            self.all_stocks[code] = Stock(code)
        stock = self.all_stocks[code]
        
        data = {
            'date': parts[0],
            'code': code,
            'open': parts[2],
            'high': parts[3],
            'low': parts[4],
            'close': parts[5],
            'volume': parts[6],
            'amount': parts[7],
            'amplitude': parts[8],
            'pct_chg': parts[9],
            'change': parts[10],
            'turnover': parts[11]
        }
        stock.update_data(data)
        
        # 若股票在持仓中,同步更新portfolio的引用
        if code in self.portfolio.positions:
            self.portfolio.positions[code] = stock
        
        return stock
    
    def process_trading_day(self, day_num: int, is_last_day: bool = False):
        """生成并输出当日委托单"""
        # 生成订单
        orders = self.strategy.generate_orders(self.all_stocks)
        
        # 输出委托
        for order in orders:
            print(order)
        print("DONE")
        sys.stdout.flush()  # 强制刷新输出
    
    def run(self):
        """主循环:处理初始化、每日行情、委托交互"""
        try:
            # 1. 读取初始化参数
            init_line = sys.stdin.readline()
            if not init_line:
                return
            self.parse_init_line(init_line)
            
            # 2. 读取基准日(day 0)行情
            for _ in range(self.n):
                line = sys.stdin.readline()
                if not line:
                    return
                self.parse_stock_line(line.strip())
            
            # 3. 每日交易循环(day 1 ~ day D)
            for day in range(1, self.D + 1):
                # 生成当日委托
                self.process_trading_day(day, day == self.D)
                
                # 读取当日行情(交易后返回的day t行情)
                day_data_count = 0
                while day_data_count < self.n:
                    line = sys.stdin.readline()
                    if not line:
                        return
                    line = line.strip()
                    if line.startswith("FINISH"):
                        # 处理最终结果
                        final_asset = float(line.split()[1])
                        profit = final_asset - self.portfolio.initial_cash
                        print(f"\n=== 最终资产: {final_asset:.2f} ===", file=sys.stderr)
                        if profit > 879950:
                            print("赚够啦!快请我吃土豆泥~", file=sys.stderr)
                        return
                    self.parse_stock_line(line)
                    day_data_count += 1
                
                # 更新投资组合每日数据
                self.portfolio.update_daily_value()
            
            # 4. 最后一日收盘后交易(若需)
            self.process_trading_day(self.D + 1, True)
            
            # 5. 读取最终结果(循环外的情况,如D+1交易后)
            final_line = sys.stdin.readline()
            if final_line and final_line.startswith("FINISH"):
                final_asset = float(final_line.split()[1])
                profit = final_asset - self.portfolio.initial_cash
                print(f"\n=== 最终资产: {final_asset:.2f} ===", file=sys.stderr)
                if profit > 879950:
                    print("赚够啦!快请我吃土豆泥~", file=sys.stderr)
                
        except Exception as e:
            print(f"运行错误: {e}", file=sys.stderr)
            import traceback
            traceback.print_exc(file=sys.stderr)

def main():
    system = TradingSystem()
    system.run()

if __name__ == "__main__":
    main()

可怜的 \(Kibrel\) ,写的是严格大于。
等会这好像映射的我自己。
我想,我应该帮他改成大于等于,也是帮我自己。

Day 12

上午

省选模拟测试

T1:tree

目测树形 \(DP\) ,而且没有部分分。思考了半天就跑了。

T2: paint(这题假完了)

概率 \(DP\),正确答案应该是用矩阵加速。我用我的 \(DP\) 式子估分得到 \(65pts\)

定义 \(f_{i, j}\) 表示在第 \(i\) 秒的时候有 \(j\) 种颜色的概率。

初始化:

\[f_{0, n} = 1 \]

状态转移方程(两部分):

\[\LARGE f_{i, j} \xrightarrow[+]{\times (\frac{1}{n} + \frac{(n - j + 1) \times (n - j)}{n ^ 2})} f_{i + 1, j} \]

\[\LARGE f_{i, j} \xrightarrow[+]{\times (\frac{n - 1}{n} - \frac{(n - j + 1) \times (n - j)}{n ^ 2})} f_{i + 1, j - 1} \]

遍历顺序:

\[\LARGE \forall i \in [0, t), \\ \large \forall j \in [1, t] \]

//Author: Kibrel

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxT = 1005, mod = 1e9+7;

int n, t, k;
int f[maxT][11];

int fpow(int a, int b) {
	int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

int inv(int x) {
	return fpow(x, mod - 2);
}

int changeProb, stayProb;

void main() {
	cin >> n >> t >> k;
	changeProb = (n - 1) * inv(n) % mod;
	stayProb = 1 * inv(n) % mod;
	f[0][n] = 1;
	for (int i = 1; i <= t; i++)
		f[i][n] = f[i - 1][n] * stayProb % mod;
	for (int i = 0; i < t; i++) {
		for (int j = 2; j <= n; j++) {
			f[i + 1][j - 1] = 
				(f[i + 1][j - 1] + 
					( f[i][j] * ( (changeProb - (n - j + 1) * (n - j) % mod * inv(n * n) % mod + mod) % mod ) % mod )
				) % mod;
		}
		for (int j = 1; j < n; j++)
			f[i + 1][j] =
				( f[i + 1][j] +
						f[i][j] * ( stayProb + (n - j + 1) * (n - j) % mod * inv(n * n) % mod ) % mod
				) % mod;
	}
	int ans = 0;
	for (int i = k; i <= n; i++)
		ans = ( ans + f[t][i] ) % mod;
	cout << ans << endl;
}

#undef int
#undef endl

}

int main() {
	freopen("paint.in", "r", stdin);
	freopen("paint.out", "w", stdout);
	
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

T3:string

不会。

T4:dices

暴力:把骰子点数放到一个环上,求这个环上的最小区间使得所有点数都被包含在内。
复杂度:\(\mathcal{O}(nqlog_2n)\)
\(10pts\)

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 2e5+5;

int n, q, m;
int a[maxn];

vector<int> getDices(int l, int r, int v) {
	set<int> res;
	for (int i = l; i <= r; i++)
		res.insert(a[i]);
	res.insert(v);
	vector<int> ret;
	for (int elem : res)
		ret.push_back(elem);
	return ret;
}

void main() {
	cin >> n >> q >> m;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= q; i++) {
		int l, r, v;
		cin >> l >> r >> v;
		auto dices = getDices(l, r, v);
		int maxL = 0;
		for (size_t i = 0; i < dices.size(); i++) {
			if (i == 0)
				maxL = max(maxL, dices.front() - (dices.back() - m));
			else
				maxL = max(maxL, dices[i] - dices[i - 1]);
		}
		cout << m - maxL << endl;
	}
}

#undef int 
#undef endl

}

int main() {
	freopen("dice.in", "r", stdin);
	freopen("dice.out", "w", stdout);
	
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

实际得分

\(29pts\)

下午

改题:string

我们需要维护这样的一个值 \(k\) ,并找到一个最短的 \(S[i:]\) 子序列,满足其无法满足 \(S[i-1:]\)的子序列。

我们判断一个串是不是另一个串的子序列的时候需要这样判定,我们有这样的前缀数组:

\[pre_{i, j} = max_{s_k = j, k < j} k \]

从后往前跳。我们可以把他抽象画成一棵树,从每一个点 \(i\)\(pre_{i, j}\) 连边,从 \(n + 1\)\(SPFA\)
我们可以知道跳到 \(i - 1\) 的最小次数就是当前这个点位的最短子序列的 \(x_i\)

那么我们只需要找到最大的 \(x_i\) 就可以知道能够满足刚好满足条件的 \(k\) 了。

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

namespace OI {

constexpr int maxn = 3e6+5;
int T;
string s;
int n, ans;
int pre[maxn][30], dis[maxn], vis[maxn];

inline void SPFA(int s) {
	queue<int> que;
	que.push(s);
	for (int i = 1; i <= n; i++)
		dis[i] = INT_MAX, vis[i] = 0;
	dis[s] = 0;
	while (!que.empty()) {
		int u = que.front();
		que.pop();
		vis[u] = 0;
		for (int i = 0, v = pre[u][i]; i < 26; v = pre[u][++i]) {
			if (dis[v] > dis[u] + 1) {
				dis[v] = dis[u] + 1;
				if (!vis[v])
					vis[v] = 1, que.push(v);
			}
		}
	}
}

void solve() {
	ans = 0;
	cin >> s;
	n = s.size();
	for (int i = 1; i <= n + 1; i++) {
		for (int j = 0; j < 26; j++)
			pre[i][j] = 0;
	}
	s = 'z' + s + 'z';
	for (int i = 1; i <= n + 1; i++) {
		for (int j = 0; j < 26; j++)
			pre[i][j] = pre[i - 1][j];
		pre[i][s[i - 1] - 'a'] = i - 1;
	}
	SPFA(n + 1);
	for (int i = 1; i <= n; i++)
		ans = max(ans, dis[i]);
	cout << ans << endl;
}

void main() {
    cin >> T;
    while (T--)
        solve();
}

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

P3901 线段树练习

我们可以找到这样的一个性质,定义 \(pre\) 为这个数字上一次出现的位置。如果 \(pre_i\) 大于你要查询的 \(l\) 就说明这个数字已经出现过了一次,那么就说明重复了。
然后我们就可以用线段树维护 \(pre\) 数组的最大值,进而一次与查询的 \(l\) 比较即可。

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

namespace OI {

#define int long long
#define endl "\n"

constexpr int maxn = 1e5+5;

int n, q;
int a[maxn], pre[maxn], app[maxn];
int seg[maxn];

// 线段树统计这些数字的 $pre$ 数组的最大值。在查询的时候发现如果最大值大于等于 $l$ 就假,否则返回真。

inline void pushUp(int x) {
	seg[x] = max(seg[2 * x], seg[2 * x + 1]);
}

void build(int x, int L, int R) {
	if (L == R) {
		seg[x] = pre[L];
		return;
	}
	int mid = L + R >> 1;
	build(2 * x, L, mid);
	build(2 * x + 1, mid + 1, R);
	pushUp(x);
}

bool query(int x, int L, int R, int l, int r) {
	if (l <= L && R <= r) {
		return seg[x] < l;
	}
	int mid = L + R >> 1;
	bool ans = 1;
	if (l <= mid)
		ans = query(2 * x, L, mid, l, r);
	if (ans && mid < r)
		ans = query(2 * x + 1, mid + 1, R, l, r);
	return ans;
}


void main() {
	cin >> n >> q;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= n; i++)
		pre[i] = app[a[i]], app[a[i]] = i;
	build(1, 1, n);
	for (int i = 1; i <= q; i++) {
		int l, r;
		cin >> l >> r;
		if (query(1, 1, n, l, r))
			cout << "Yes" << endl;
		else
			cout << "No" << endl;
	}
}

#undef int
#undef endl

}

int main() {
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	
	OI::main();
	
	return 0;
}

Christmas

今天是圣诞节欸!

上午打了个模拟赛,但是其实已经没什么心情了。最后不出意外的爆零了。

在返乡的火车上,我们先是颓麻将,后来我身体不大舒服,便望着窗外的风景睡去了。渐渐的,我醒来了,感觉过去的十多天仿佛幻梦一般 —— 好像我只是从一开始的火车上醒来了一样。

或许,我只是坐上了一趟开往圣诞节的火车吧。

\[\LARGE Merry~Christmas! \]

圣诞树

完结撒花!

posted @ 2025-12-12 23:14  Kibrel  阅读(104)  评论(2)    收藏  举报