20250713 总结
P12431 [BalticOI 2025] Gingerbread
托伦自中世纪以来就以传统姜饼闻名。年轻的尼古拉斯想在他最喜欢的商店购买一套装有姜饼饼干的 n 个盒子。不过这家店有非常严格的规定:尼古拉斯最初会得到 n 个已经装有饼干的盒子,其中第 i 个盒子初始装有 ai 块饼干。然后,尼古拉斯可以订购一些额外的饼干。他需要向某些盒子中添加额外的饼干,使得所有盒子中饼干数量的最大公约数*等于 1。可以证明这总是可行的。
请帮助尼古拉斯计算出,为了使所有饼干数量的最大公约数等于 1,需要添加的最少饼干数量。
- 多个数的最大公约数(GCD)是指能整除所有这些数的最大正整数。
打表
发现n大于3的时候答案不会超过2
令 gi=gcd(a1,…,ai−1,ai+1,…,an),会存在两种情况:
∀i∈[1,n],gcd(gi,ai+1)=1,则需进行任意 2 次操作。
否则,仅需进行 1 次操作。
n小于等于3直接暴力即可。
关于为什么需要小于2次操作的证明:
一些废话
结论1:如果 ∀i ∈ [1,n], gcd(g_i, a_i+1) ≠ 1,那么 1 次操作无法使 GCD 变为 1。
证明: (废话)反证法。假设存在某次操作(给第 j 个位置加 k 块饼干)使得新数组的 GCD 为 1。
新数组为:a_1, a_2, ..., a_{j-1}, a_j+k, a_{j+1}, ..., a_n 新 GCD = gcd(a_1, a_2, ..., a_{j-1}, a_j+k, a_{j+1}, ..., a_n) = gcd(g_j, a_j+k) = 1
这与条件 gcd(g_j, a_j+k) ≠ 1 矛盾(当 k=1 时)。
对于 k > 1,我们需要更细致的分析,但关键观察是:如果 gcd(g_j, a_j+1) > 1,那么通常 gcd(g_j, a_j+k) 也会 > 1(除非 k 恰好选择得很特殊)。
主要证明:2 次操作足够
当 n ≥ 3 且 ∀i ∈ [1,n], gcd(g_i, a_i+1) ≠ 1 时,2 次操作总是足够的。
证明:
设 d = gcd(a_1, a_2, ..., a_n) = p_1^{α_1} · p_2^{α_2} · ... · p_t^{α_t},其中 p_1, p_2, ..., p_t 是不同的质数。
条件 ∀i, gcd(g_i, a_i+1) ≠ 1 意味着对于每个位置 i,都存在某个质因数同时整除 g_i 和 a_i+1。
构造 2 次操作:
选择两个不同的位置 i, j(比如 i=1, j=2)
给 a_1 加 1,给 a_2 加 1
新数组变为:a_1+1, a_2+1, a_3, ..., a_n
设 p 是 d 的任意质因数。我们需要证明新数组中至少有一个数不被 p 整除。
如果 p ∤ (a_1+1),则完成
如果 p ∤ (a_2+1),则完成
如果 p | (a_1+1) 且 p | (a_2+1),那么:
p | a_1+1 意味着 a_1 ≡ -1 (mod p)
p | a_2+1 意味着 a_2 ≡ -1 (mod p)
但是 p | d 意味着 p | a_1 和 p | a_2,即 a_1 ≡ 0 (mod p) 和 a_2 ≡ 0 (mod p)
这导致 0 ≡ -1 (mod p),即 p | 1,因此 p = 1
这是矛盾的,因为 p 是质因数且 p > 1。
P12426 [BalticOI 2025] BOI acronym
众所周知,BOI 是 Baltic Olympiad in Informatics(波罗的海信息学奥林匹克竞赛)名称的缩写。
主办方认为缩写 BOI 太容易发音(毕竟它在英语中是一个单音节词)。因此,他们提出了一个新的缩写。为了与其他区域性奥林匹克竞赛(如 CEOI)轻松区分,新缩写仍然仅由字符 "B"、"O" 和 "I" 组成。此外,"B" 必须是缩写中严格最常见的字符。也就是说,"B" 的出现次数严格多于 "O",同时也严格多于 "I"。
例如,缩写 "OBOIIBBB" 和 "B" 是有效的,但 "IBIIBB"、"BOI"、"O" 和 "BCB" 不是。
为了让事情更有趣,主办方没有直接公布完整的缩写,而是只提供了一些提示。具体来说,对于新缩写的每个连续子串,他们给出了该子串中最常见字符的出现次数。注意,这个字符不一定是 "B",而且最常见字符也不一定是唯一的。令人惊讶的是,可以证明这些信息实际上足以还原所有 "B" 的出现位置。你能找到它们吗?
步骤一:确定第一个和最后一个 'B'
首先,我们从输入矩阵 M 中获取全局信息。M[1][n] 是整个字符串 S[1...n] 的众数出现次数。根据题意,'B' 是严格最常见的字符,因此字符串中 'B' 的总数 total_b 就等于 M[1][n]。
寻找第一个 'B' 的位置 l_b: 考虑子串 S[i...n]。如果 S[i-1] 不是 'B',那么 S[i...n] 中的 'B' 数量仍然是 total_b,其众数出现次数 M[i][n] 很可能也等于 total_b。但如果 S[i-1] 是 'B',那么 S[i...n] 中的 'B' 数量会减少为 total_b - 1,这几乎必然导致 M[i][n] 的值变小。 因此,第一个 'B' 的位置 l_b 就是满足 M[l_b+1][n] < total_b 的最小 l_b。我们可以通过一个循环找到它: l_b = 1; while (l_b < n && M[l_b + 1][n] == total_b) l_b++;
寻找最后一个 'B' 的位置 r_b: 同理,最后一个 'B' 的位置 r_b 是满足 M[l_b][r_b-1] < total_b 的最大 r_b。注意,这里的左边界应该是我们刚刚找到的 l_b,而不是固定的 1,这样逻辑才严谨。 r_b = n; while (r_b > l_b && M[l_b][r_b - 1] == total_b) r_b--;
现在,我们确定了所有 'B' 都分布在闭区间 [l_b, r_b] 内。我们的任务缩减为判断 (l_b, r_b) 开区间内的每个位置。
步骤二:迭代判断与充要条件
我们从 p = l_b + 1 遍历到 r_b - 1。在判断位置 p 时,我们已经知道了 [l_b, p-1] 区间内所有 'B' 的位置和数量。设这个数量为 count_b_prefix。同时,我们也可以推断出在后缀 [p, r_b] 中 'B' 的总数为 count_b_suffix = total_b - count_b_prefix。
S[p] 是 'B',当且仅当以下三个条件中至少一个成立:
左侧驱动 (Left-Side Driven): S[p] 的加入,使得 'B' 成为 S[l_b...p] 这个前缀子串的众数。这意味着 S[p] 与 [l_b, p-1] 中的 count_b_prefix 个 'B' 汇合,形成了 count_b_prefix + 1 的规模。为了确保这种优势是 S[l_b] 和 S[p] 共同贡献的,我们还需要验证移除 S[l_b] 后,'B' 的数量会降回去。
条件: (M[l_b][p] == count_b_prefix + 1) && (M[l_b + 1][p] == count_b_prefix)
右侧驱动 (Right-Side Driven): S[p] 的存在对于 S[p...r_b] 这个后缀子串的 'B' 成为众数是至关重要的。一个简洁而强大的判断方式是:如果没有 S[p],即在子串 S[p+1...r_b] 中,'B' 无法成为众数。该子串中 'B' 的数量为 count_b_suffix - 1。
条件: M[p + 1][r_b] < count_b_suffix
间接证明 (Indirect Proof): 此条件处理的是最微妙的情况:'B' 在 p 的左、右局部子串中都不是众数。我们的推理逻辑是排除法:如果能证明 S[p] 既不是其左侧子串的众数(比如 'O'),也不是其右侧子串的众数(比如 'I'),那么它就只能是 'B'。
如何证明 S[p] 不是左侧的众数? 我们考察子串 S[l_b...p-1]。首先,通过 M[l_b][p - 1] > count_b_prefix 这个子条件,我们确认 'B' 在该子串中不是众数,所以众数必然是 'O' 或 'I'。然后,通过 M[l_b][p] == M[l_b][p - 1] 这个子条件,我们发现加入 S[p] 之后,众数的出现次数没有增加。这有力地说明了 S[p] 与 S[l_b...p-1] 的众数不是同一种字符。
如何证明 S[p] 不是右侧的众数? 我们用类似的逻辑考察右侧子串。M[p][r_b] == M[p+1][r_b-1] 是一个精巧的表达,它同样暗示了 S[p] 的存在没有加强右侧的众数。
综上,当这组条件满足时,我们实际上完成了一次逻辑排除:S[p] 不是左侧的众数('O'),也不是右侧的众数('I'),因此它必然是 'B'。
条件: (M[l_b][p] == M[l_b][p - 1]) && (M[l_b][p - 1] > count_b_prefix) && (M[p][r_b] == M[p+1][r_b-1])
在循环中,只要 p 满足这三个条件的任意一个,我们就将其判定为 'B',并更新 count_b_prefix。
P11491 [BalticOI 2023] Tycho
你被给定 n 个在平面坐标上的点。
写一个程序计算出一个最大的矩形区域,使它的每一个顶点都是给定的点之一。你可以假设至少存在一个这样的区域。
输入格式
第一行,一个整数 n,表示点的数量。
以下 n 行,每行两个整数,表示一个点的坐标。 坐标值在 −108 与 108 之间。
没有两个点位于同一个坐标。
一个朴素的暴力是枚举第一个,第二各,低三个,第四个端点,但是这个是n^4且没有前途的,因为你无法优化。
矩形有很多性质,都可以利用(不同解法),这里我们利用:矩形的两条对角线互相平分并且长度相等。
先列出来所有线段,然后枚举第一条和第二条,虽然也是n^4的,但是显然有了可以优化的空间。
我们把线段第一关键字长度,坐标作为第二关键字,显然枚举的时候直接想后跑,打tag,这样可以做到类似O(n^2)的复杂度,但是由于你排序是log n^2的,所以你怎么做都行。
P11491 [BalticOI 2023] Tycho
首先,我们定义一个基础的DP状态: f[i] = 到达第 i 个安全点 a_i 所需的最小总成本(时间+罚款)。
为了方便,我们设 a_0 = 0,则初始状态为 f[0] = 0。
要计算 f[i],我们需要从之前任意一个安全点 a_j (其中 0 <= j < i) 转移而来。状态转移方程为:
f[i] = min_{0 <= j < i} { f[j] + Cost(j -> i) }
其中 Cost(j -> i) 是从 a_j 移动到 a_i 这一段路程所产生的额外成本。这个成本包含两部分:
时间成本: a_i - a_j
罚款成本: 在这段 a_i - a_j 的路程中,我们会经过 floor((a_i - a_j - 1) / p) 个罚款检查点。因此罚款为 floor((a_i - a_j - 1) / p) * d。
将 Cost(j->i) 代入,我们得到一个完整的、但效率不高的 O(N^2) DP转移方程:
f[i] = min_{0 <= j < i} { f[j] + (a_i - a_j) + floor((a_i - a_j - 1) / p) * d }
--- (方程 1)
第2步:对 floor 项进行代数分解
方程1的瓶颈在于 floor 项同时与 i 和 j 相关,无法快速找到最优的 j。我们的目标是把和 i 相关的项与和 j 相关的项分离开。
我们利用带余除法来分解 a_i 和 a_j:
a_i = q_i * p + r_i (其中 q_i = a_i / p, r_i = a_i % p)
a_j = q_j * p + r_j (其中 q_j = a_j / p, r_j = a_j % p)
将它们代入 floor 项: floor((a_i - a_j - 1) / p) = floor( ( (q_ip + r_i) - (q_jp + r_j) - 1 ) / p ) = floor( ( (q_i - q_j)*p + (r_i - r_j - 1) ) / p ) = q_i - q_j + floor( (r_i - r_j - 1) / p )
现在分析 floor( (r_i - r_j - 1) / p )。因为 0 <= r_i, r_j < p,所以 -p < r_i - r_j < p。 我们根据 r_i 和 r_j 的关系分情况讨论:
情况A: r_i > r_j
此时 r_i - r_j >= 1,所以 r_i - r_j - 1 >= 0。
因为 r_i - r_j - 1 < p - 1,所以 0 <= (r_i - r_j - 1) / p < 1。
因此,floor( (r_i - r_j - 1) / p ) = 0。
情况B: r_i <= r_j
此时 r_i - r_j <= 0,所以 r_i - r_j - 1 < 0。
因为 r_i - r_j - 1 > -p,所以 -1 <= (r_i - r_j - 1) / p < 0。
因此,floor( (r_i - r_j - 1) / p ) = -1。
第3步:将分解结果代回原方程
现在,我们将分解后的 floor 项代回到方程1中。 f[i] = min_{0 <= j < i} { f[j] + (a_i - a_j) + (q_i - q_j + floor(...)) * d } f[i] = min_{0 <= j < i} { f[j] - a_j - q_jd + a_i + q_id + floor(...) * d }
我们把方程拆成上面讨论的两种情况:
对于满足 r_j < r_i 的 j (情况A): f[i] = min { f[j] - a_j - q_j*d + a_i + q_i*d } f[i] = a_i + q_i*d + min_{j | r_j < r_i} { f[j] - a_j - q_j*d }
--- (方程 2A)
对于满足 r_j >= r_i 的 j (情况B): f[i] = min { f[j] - a_j - q_j*d + a_i + q_i*d - d } f[i] = a_i + q_i*d - d + min_{j | r_j >= r_i} { f[j] - a_j - q_j*d }
--- (方程 2B)
f[i] 的最终值就是这两个方程算出的结果中的较小者。
第4步:整理成线段树优化的最终形式
观察方程2A和2B,我们发现 min 内部的表达式 { f[j] - a_j - q_j*d } 是一个只与 j 相关的固定值。我们可以用一个数据结构来维护这个值,并根据 r_j 的范围进行区间查询。这个数据结构就是线段树。
我们定义线段树需要维护的值 V[j]: V[j] = f[j] - a_j - (a_j / p) * d
现在重写方程2A和2B:
情况A (r_j < r_i): f[i] = a_i + (a_i/p)*d + min_{j | 0 <= r_j < r_i} { V[j] }
情况B (r_j >= r_i): f[i] = a_i + (a_i/p)*d - d + min_{j | r_i <= r_j < p} { V[j] }
这两个方程就是我们可以直接用于编程的、可被线段树优化的最终形式。
线段树优化流程
结合最终方程,我们可以清晰地看到每一步的线-段树操作:
在计算 f[i] (即循环到第 i 个安全点 a_i) 时:
查询最小值部分 (Query)
为了计算 f[i],我们需要从线段树中查询两部分的历史最优值:
min_A = ask(0, r_i - 1): 查询余数在 [0, r_i - 1] 区间内的 V[j] 的最小值。
min_B = ask(r_i, p - 1): 查询余数在 [r_i, p - 1] 区间内的 V[j] 的最小值。
根据查询结果,计算两种情况下的 f[i] 候选值:
f_A = a_i + (a_i/p)*d + min_A
f_B = a_i + (a_i/p)*d - d + min_B
f[i] = min(f_A, f_B)
更新单点部分 (Update)
我们已经计算出了最优的 f[i]。现在,为了让 a_i 能作为后续计算的决策点,我们需要计算出它对应的 V[i] 值,并更新到线段树中。
计算 V[i]: V[i] = f[i] - a_i - (a_i / p) * d
更新线段树: upd(r_i, V[i]): 在线段树的 r_i (即 a_i % p) 位置上,更新其值为 V[i]。
通过以上“查询-更新”的循环,我们就可以在 O(N log P) 的时间内完成整个DP计算。
浙公网安备 33010602011771号