<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>SISTINE=Blog</title><link>https://SistineFibel-2006.github.io</link><description>QQ3034019410——Atcoder:SistineFibel</description><copyright>SISTINE=Blog</copyright><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://img.cdn1.vip/i/6a3aa678f126b_1782228600.webp</url><title>avatar</title><link>https://SistineFibel-2006.github.io</link></image><lastBuildDate>Fri, 11 Sep 2026 16:12:21 +0000</lastBuildDate><managingEditor>SISTINE=Blog</managingEditor><ttl>60</ttl><webMaster>SISTINE=Blog</webMaster><item><title>20260911 每日 1.0 3.0 4.0（未完成❎）</title><link>https://SistineFibel-2006.github.io/post/20260911%20-mei-ri-%201.0%203.0%204.0%EF%BC%88-wei-wan-cheng-%E2%9D%8E%EF%BC%89.html</link><description># 20260911 每日 1.0 3.0 4.0

---

---

## No.773 竞赛

https://yukicoder.me/problems/no/773

&gt; [!note]
&gt;
&gt; 给定无法参赛的时间 $[A,B]$，问 $23, 24,25$ 日能参加几天

随便写一下

```cpp
namespace XK {
  void solve() {
  	int A, B; in(A, B);
  	V&lt;int&gt; x(32, 0);
  	Rep(i, A, B + 1) x[i] = 1;
  	ll ans = 0;
  	Rep(i, 23, 26) if(x[i]) ans ++;
  	out(3 - ans);
  }
};
```

---

## No.3561 收集 KCPC

https://yukicoder.me/problems/no/3561

&gt; [!note]
&gt;
&gt; 一个有向图上，有边权，每个点有一个字母，问能不能找到一条从 $1$ 开始，每一次可选收不收集点上的字母，手中的字母按顺序构成 `KCPC`。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/20260911%20-mei-ri-%201.0%203.0%204.0%EF%BC%88-wei-wan-cheng-%E2%9D%8E%EF%BC%89.html</guid><pubDate>Fri, 11 Sep 2026 16:11:50 +0000</pubDate></item><item><title>20260910 每日 2.0 2.5 5.0</title><link>https://SistineFibel-2006.github.io/post/20260910%20-mei-ri-%202.0%202.5%205.0.html</link><description># 20260910 每日 2.0 2.5 5.0

---

---

## No.1623 三角形的制作 2.0$

https://yukicoder.me/problems/no/1623

&gt; [!note]
&gt;
&gt; 红色，绿色，蓝色棒子各 $N$ 根，第 $i$ 根长度为 $r_i, g_i, b_i$
&gt;
&gt; 求使用第 $i$ 个红棒，第 $j$ 个绿棒，第 $k$ 个蓝棒制作三角形且满足，$r_i=max(r_i, g_i, b_k)$ 的整数对数量
&gt;
&gt; - $1 \le N \le 2 \times 10^5$  $1 \le r_i,g_i,b_i \le 3 \times 10^3$

显然枚举两组然后二分第三组是不能通过的，于是考虑值域

看到了值域可以满足平方复杂度，于是考虑枚举绿和蓝的数量，然后对红棒前缀和，直接相减后统计计数即可。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/20260910%20-mei-ri-%202.0%202.5%205.0.html</guid><pubDate>Thu, 10 Sep 2026 14:17:31 +0000</pubDate></item><item><title>20260901 每日 1.5 2.5 4.0</title><link>https://SistineFibel-2006.github.io/post/20260901%20-mei-ri-%201.5%202.5%204.0.html</link><description># 20260901 每日 1.5 2.5 4.0

---

---

## No.3141 Banlancing with X=&gt;O Flip 1.5$

https://yukicoder.me/problems/no/3141

&gt; [!note]
&gt;
&gt; 给一个长度为 $N$ 括号序列 $S$，问能否通过大于等于 0 次下面操作，把 $S$ 变为合法括号串
&gt;
&gt; - 将 $S$ 中子串包含的 `)(` 替换成 `()`。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/20260901%20-mei-ri-%201.5%202.5%204.0.html</guid><pubDate>Tue, 01 Sep 2026 13:55:13 +0000</pubDate></item><item><title>20260831 每日 2.0 2.5 4.0</title><link>https://SistineFibel-2006.github.io/post/20260831%20-mei-ri-%202.0%202.5%204.0.html</link><description># 20260831 每日 2.0 2.5 4.0

---

## No.741 AscNumber 不下降数字 2.0$

https://yukicoder.me/problems/no/741

&gt; [!note]
&gt;
&gt; 给定正整数 $1 \le N \le 10^6$ ，找出所有比 $10^N$ 小的**不下降数字$^1$**
&gt;
&gt; **不下降数字**：一个非负整数，其十进制表示数各个数位从左往右读不下降

比较显然的一个数位 DP，两星还挺合适的

定义 $dp[i][j] :=$ 长度为 $i$ 且最后一位是 $j$ 的不下降数字方案数

转移方程显然是 $dp[i][j] = \sum_{k \le j} dp[i - 1][k]$

初始值就是 $i \in[0, 9],$ $dp[1][i] = 1$

统计结果就是 $\sum_{i = 0}^9 dp[N][i]$

可以使用滚动数组优化，空间复杂度是常数，时间复杂度是 $O(10N)$

```cpp
//no-unlocked
ll f[1d6 + 2][10];
{
	ll @N;
	rep(i, 10) f[1][i] = 1;
	rep(i, 1, N + 1) {
		rep(j, 10) rep(k, j + 1) (f[i + 1][j] += f[i][k]) %= MD;
	}
	ll ans = 0;
	rep(i, 10) (ans += f[N][i]) %= MD;
	wt(ans);
}
```

---

---

## No.3159 Just Answer 10 Integers! 2.5$

https://yukicoder.me/problems/no/3159

&gt; [!note]
&gt;
&gt; 给定 $2 \le N \le10$，请你构造一个大小是 $N$ 的正整数不可重集合 $S$，使得其分数$^1$最小。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/20260831%20-mei-ri-%202.0%202.5%204.0.html</guid><pubDate>Mon, 31 Aug 2026 12:24:04 +0000</pubDate></item><item><title>20260826 每日一题：1.5 3.0 4.0</title><link>https://SistineFibel-2006.github.io/post/20260826%20-mei-ri-yi-ti-%EF%BC%9A1.5%203.0%204.0.html</link><description># Yukicoder 20260826 每日

## 1- No.2708 Jewel holder  1.5$

[No.2708 Jewel holder - yukicoder](https://yukicoder.me/problems/no/2708)

### **问题描述**

有一个纵向 $H$ 行、横向 $W$ 列的网格。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/20260826%20-mei-ri-yi-ti-%EF%BC%9A1.5%203.0%204.0.html</guid><pubDate>Sat, 29 Aug 2026 07:17:28 +0000</pubDate></item><item><title>2026 上海市赛 E 完美区间</title><link>https://SistineFibel-2006.github.io/post/2026%20-shang-hai-shi-sai-%20E%20-wan-mei-qu-jian.html</link><description># QOJ19007 2026 上海市赛 E 完美区间

[完美区间 - 题目 - QOJ.ac](https://qoj.ac/contest/3944/problem/19007)

&gt; [!note]
&gt;
&gt; 给你长度为 $n$ 的整数序列 $S$，满足序列内元素两两互质，完成 $T$ 组查询或修改操作：
&gt;
&gt; - 查询：给你一个区间 $[l, r]$，问是否是一个完美区间；指的是，对于每一个数 $x$ 在区间内，我们记录他的出现次数是 $c_x$，都有 $c_x \mod{x} = 0$
&gt; - 修改：把一个位置 $p$ 上的 $S_p := x$，保证修改后也是两两互质的
&gt;
&gt; **「约束」**：$n, T \le 200000, \ \ x,S_i \le 10^8$



好牛逼的 Ad-hoc！

如果你能想到一个数 $x$ 出现了其值的倍数次数一定有 $x^{-1} \times c_x$ 是一个整数的话，你就自然可以想到：

- 实际上就是 $t = x^{-1} \times c_x$  的话，有 $t - round(t) = 0$，对于每个 $x$ 都满足

然后你要能想到，如果两两互质的话，他乘起来就不会干扰，于是有：

- $t = pre[r] - pre[l - 1]$ ，如果有 $t- round(t) = 0$ ，那么就是 `Yes`，反之是 `No`

这题就写完了，

你只需要维护一个 $x^{-1}$ 的任何一个可以修改查询前缀和的东西就行了

```cpp
//no-unlocked

const ll M1 = 2013265921, M2 = 2281701377;
{
	int @n, @T;
	fenwick&lt;ll&gt; t1, t2;
	t1.malloc(n + 1); t1.init(n + 1); t2.malloc(n + 1); t2.init(n + 1);
	ll a[n];
	rep(i, n) {
		ll @x; a[i] = x;
		t1.add(i, powmod(a[i], M1 - 2, M1)); t2.add(i, powmod(a[i], M2 - 2, M2));
	}

	rep(T) {
		int @op;
		if(op == 0) {
			int @l, @r; l --, r --;
			ll r1 = t1.get(r), r2 = t2.get(r);
			ll l1 = t1.get(l - 1), l2 = t2.get(l - 1);
			wt((r1 - l1 + M1) % M1 == (r2 - l2 + M2) % M2 ? 'yes' : 'no');
		} else {
			int @p; ll @x; p --;
			t1.add(p, -powmod(a[p], M1 - 2, M1));
			t2.add(p, -powmod(a[p], M2 - 2, M2));
			a[p] = x;
			t1.add(p, powmod(a[p], M1 - 2, M1));
			t2.add(p, powmod(a[p], M2 - 2, M2));
		}
	}
}
// 代码是 clay
```

。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/2026%20-shang-hai-shi-sai-%20E%20-wan-mei-qu-jian.html</guid><pubDate>Thu, 20 Aug 2026 18:00:44 +0000</pubDate></item><item><title>NowCoder-SummerDay4-F</title><link>https://SistineFibel-2006.github.io/post/NowCoder-SummerDay4-F.html</link><description># NowCoder-SummerDay4F: [23 Subsequences](https://ac.nowcoder.com/acm/contest/133879/F)

题目 ：[23 Subsequences](https://ac.nowcoder.com/acm/contest/133879/F)

&gt; [!note]
&gt;
&gt; 定义一个序列 $B=(b_1,b_2,\ldots,b_M)$ 是好的，当且仅当对于每一个 $i=2,3,\ldots,M$，都有
&gt; $$
&gt; 2 \cdot b_{i-1} \le b_i \le 3 \cdot b_{i-1}
&gt; $$
&gt; 特别的，长度为 $1$ 的序列总是好的。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/NowCoder-SummerDay4-F.html</guid><pubDate>Wed, 29 Jul 2026 18:11:45 +0000</pubDate></item><item><title>HDU-SummerDay2- 1011 solution</title><link>https://SistineFibel-2006.github.io/post/HDU-SummerDay2-%201011%20solution.html</link><description># HDU-SummerDay2- 1011

## [1011 键盘杀手](https://acm.hdu.edu.cn/contest/problem?cid=1230&amp;pid=1011)

&gt; [!note]
&gt;
&gt; 长度为 $N$ 的序列 $A = (a_1, a_2, \ldots, a_N)$，你可以每次选择一个下标 $i$，然后消耗 $max(a_{i-1}, a_{i+1})$ 来让 $a_i = 0$，问将所有数字变成 $0$ 的最小消耗
&gt;
&gt; **「约束」**
&gt;
&gt; - $1 \le N \le 10^5$
&gt; - $1 \le a_i \le 10^9$

首先找到建模，对于一个数字 $a_i$ 而言，每个数字只有一个出度，至多两个入度，于是对于一个点，在考虑拔出顺序后，有下面四种情况：

- $i-1 \rightarrow i \leftarrow i + 1$ ：左右先被拔，$cost = 0$
- $i-1 \leftarrow i \rightarrow i + 1$ ：左右后被拔，$cost = max(a_{i-1},a_{i+1})$
- $i-1 \rightarrow i \rightarrow i + 1$ ：左先右后，$cost = a_{i+1}$
- $i-1 \leftarrow i \leftarrow i + 1$ ：左后右先，$cost = a_{i-1}$

我们可以围绕第 $i$ 个箭头方向来 DP，$f_i$ 表示由 $i - 1 \leftarrow i$，$g_i$ 表示由 $i-1 \rightarrow i$。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/HDU-SummerDay2-%201011%20solution.html</guid><pubDate>Thu, 23 Jul 2026 15:15:31 +0000</pubDate></item><item><title>NowCoder-SummerDay2-L &amp; B solution</title><link>https://SistineFibel-2006.github.io/post/NowCoder-SummerDay2-L%20%26%20B%20solution.html</link><description># NowCoder-SummerDay2-L &amp; B solution

## B : [B-Bitwise Maximization_2026牛客暑期多校训练营2](https://ac.nowcoder.com/acm/contest/133877/B)

&gt; [!note]
&gt;
&gt; $N$ 个数，分成两组 $A,B$，最大化 $\bigoplus_{i \in A} a_i + \bigoplus_{i \in B} a_i$ 
&gt;
&gt; **「约束」**
&gt;
&gt; - $1 \le N \le 5 \times 10^5$
&gt; - $0 \le a_i \le 2^{30}$

如果我们分别认为前半和后半部分的异或和为 $X, Y$，答案可以写成：
$$
X + Y = X \oplus Y + (X \land Y) \times 2
$$
于是我们发现，这个新式子的前半是 $\bigoplus_{i \in N} a_i = sum$，所以我们只要最大化后半段即可！

而我们会发现，$sum$ 中是 $1$ 的位一定会被分成 $1$ 和 $0$，而其对第后半的式子贡献总是 $0$；而 $sum$ 中是 $0$ 的位可以被分成 $1$ 和 $1$，或是 $0$ 和 $0$ 。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/NowCoder-SummerDay2-L%20%26%20B%20solution.html</guid><pubDate>Wed, 22 Jul 2026 15:06:48 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-4 区间 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-4%20-qu-jian-%20DP.html</link><description>###  跟 Nachia DP 博客学DP-4 区间 DP

如果存在一个长度为 $N$ 的序列，那么将所有连续子序列（子串）作为计算对象的 DP 称为区间 DP。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-4%20-qu-jian-%20DP.html</guid><pubDate>Tue, 14 Jul 2026 15:46:05 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-15 回退 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-15%20-hui-tui-%20DP.html</link><description>### 跟 Nachia DP 博客学DP-15 回退 DP

**回退 DP （Restore DP / Rollback DP / Erasure DP）**是一种在计数 DP （尤其是背包 DP） 中非常经典且高效的技巧

在完全计算出 DP 后，通过部分地执行与原始 DP 相反的步骤来达成目标的解法，被称为“回退 DP”

它的核心思想是：**先用 $O(NM)$ 的时间求出包含所有元素的全局 DP 状态，然后利用“逆运算”，在 $O(M)$ 的时间内快速扣除（回退）某一个特定元素的影响**

如果没有这个技巧，我们为了求“去掉第 $i$ 个元素后的答案”，必须对剩余的 $N-1$ 个元素重新跑一次 $O(NM)$ 的背包，总复杂度会劣化到 $O(N^2 M)$。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-15%20-hui-tui-%20DP.html</guid><pubDate>Tue, 14 Jul 2026 13:44:57 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-13&amp;14 多维 DP in-place DP 优化</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-13%2614%20-duo-wei-%20DP%20in-place%20DP%20-you-hua.html</link><description>### 跟 Nachia DP 博客学DP-13&amp;14 多维 DP in-place DP 优化

#### 多维 DP （*d* 次元 DP）

*d* 中是一个具体的正整数，也就是通过 in-place 优化前的维数，该维数也是 DP **时间复杂度的下界**

##### *d* 的意义

其本质是：**为了满足「无后效性」，你最少需要用几个独立的变量来唯一确定一个子问题？**

常见的一维（*d* = 1）有：斐波那契数列、LIS 问题、最大子段和；状态往往只与一个线性位置或阶段有关

常见的二维（*d* = 2）有：01背包问题、LCS 问题、区间 DP、简单网格图路径；状态由两个独立变量约束（如：位置+容量，序列A长度+序列B长度，区间左端点+右端点）。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-13%2614%20-duo-wei-%20DP%20in-place%20DP%20-you-hua.html</guid><pubDate>Tue, 14 Jul 2026 11:49:06 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-12 子序列 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-12%20-zi-xu-lie-%20DP.html</link><description>### 跟 Nachia DP 博客学DP-12 子序列 DP

**原文**：[DP 的俗称 | Mathenachia --- DP の俗称 | Mathenachia](https://www.mathenachia.blog/dp/#toc15)

对于某个序列的（不一定连续的）所有子序列，在去除重复后进行处理的典型 DP 被称为**子序列 DP**。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-12%20-zi-xu-lie-%20DP.html</guid><pubDate>Tue, 14 Jul 2026 08:33:26 +0000</pubDate></item><item><title>AtCoder ABC466E</title><link>https://SistineFibel-2006.github.io/post/AtCoder%20ABC466E.html</link><description>### [AtCoder ABC 466E - Range Flip](https://atcoder.jp/contests/abc466/tasks/abc466_e)

[E - Range Flip](https://atcoder.jp/contests/abc466/tasks/abc466_e)

#### 题目大意

给你 $N$ 张卡片，编号从 $1$ 到 $N$

第 $i$ 张卡正面写着 $A_i$，背面写着 $B_i$，一开始都是正面朝上

你可以进行下列操作至多 $K$ 次；

-  选择一个二元组 $(l, r)$ 满足 $1\le l \le r \le N$，对于所有 $l\le i\le r$ 的 $i$，翻转编号 $i$ 的卡，也就是正变反，反变正

进行完操作后，找到所有朝上数字之和的最大值

##### 约束

- $1\le N \le 2 \times 10^5$
- $1\le K \le 10$
- $1\le A_i,B_i \le 10^9$

#### 题解1

这个方法其实比较直观，如同之前的 Game DP 一样，我们设最大化变化值即可，如果定义 $d[i] = B[i] - A[i]$ 的话，那么我们要求 $max \set{\sum_{i=1}^N p \times d[i]}$，其中 $p = 0 / 1$

于是我们可以列出转移方程，设 $dp[i][j]=$ 进行了 $i$ 次操作，进行到第 $j$ 个数字时候的最大化变化值：

$dp[i][j] = max_{1 \le k \le j} \set{dp[i - 1][k - 1] + \sum_{i=k}^N d[i]}$

这个式子前缀和优化后可以变成

$dp[i][j] = max_{1 \le k \le j} \set{dp[i - 1][k - 1] + pre[j] - pre[k - 1]}$

而 $pre[j]$ 可以提出来变成

$dp[i][j] = pre[j] + max_{1 \le k \le j} \set{dp[i - 1][k - 1] - pre[k - 1]}$

而这个式子用一个 trick 就可以在 $O(NK)$ 的情况下维护了，也就是一直维护 max 值，代码如下：

```cpp
auto Mainsol = [](){
  INT(N, K);
  vec(ll, A, N); vec(ll, B, N);
  rep(i, N) in(A[i], B[i]);
  vec(ll, d, N);
  rep(i, N) d[i] = B[i] - A[i];
  vec(ll, pre, N + 1, 0);
  rep(i, N) pre[i + 1] = pre[i] + d[i];

  v&lt;v&lt;ll&gt;&gt; f(K + 1, v&lt;ll&gt;(N + 1, 0));
  rep(i, 1, K + 1) {
  	ll now = -LINF;
  	rep(j, 1, N + 1) {
  		chmax(now, f[i - 1][j - 1] - pre[j - 1]);
  		f[i][j] = f[i][j - 1];
  		chmax(f[i][j], pre[j] + now);
  	}
  }
  ll bst = 0;
  rep(i, N) bst += A[i];
  ll maxx = 0;
  rep(i, K + 1) chmax(maxx, f[i][N]);
  out(maxx + bst);
};

```

#### 题解2

这个做法需要一些观察，首先我们可以注意到，如果每一次选择的区间总是独立（也就是没有共用元素）的话，这样的选法总是优于（至少不劣）有共用元素的选法，于是我们可以注意到：

对于一个元素而言，其翻转次数最多 $K$ 次，也就是可达状态数 $2 \times K$ 种，于是我们有转移状态：

$dp[k] =$ 处理完前 $i$ 个元素后，进行了 $k$ 次切换能达到的最大总和

于是可以写出代码

每一次先尝试切换状态，如果前一个状态更优，那么就切换，然后根据奇偶加对应的结果即可，类似 状态机 DP！

```cpp
// code of potato167 
// https://atcoder.jp/contests/abc466/submissions/77364802
void solve(){
    int N, K;
    cin &gt;&gt; N &gt;&gt; K;
    vector&lt;ll&gt; A(N), B(N);
    rep(i, 0, N) cin &gt;&gt; A[i] &gt;&gt; B[i];
    vector&lt;ll&gt; dp(K * 2 + 1);
    rep(i, 0, N) {
        rep(j, 1, dp.size()) {
            chmax(dp[j], dp[j - 1]);
        }
        rep(j, 0, dp.size()) {
            if (j &amp; 1) dp[j] += B[i];
            else dp[j] += A[i];
        }
    }
    cout &lt;&lt; vec_max(dp) &lt;&lt; '\n';
}
```

。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/AtCoder%20ABC466E.html</guid><pubDate>Sun, 12 Jul 2026 18:43:49 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-11 换根 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-11%20-huan-gen-%20DP.html</link><description>### 跟 Nachia DP 博客学DP-11 ：换根 DP

将自底向上的树形 DP 的结果对任意根节点一次性计算的 DP 法，叫做**全方位树形 DP（换根 DP）**

从叶子向根计算树 DP 后，对于每个顶点 $v$，将 $v$ 的子节点排成一列并双向计算累积值，这样按照从根到叶子的顺序，就能以每个顶点常数次合并的方式实现与树 DP 相同的聚合。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-11%20-huan-gen-%20DP.html</guid><pubDate>Sat, 04 Jul 2026 16:30:44 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-10  平方的树形 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-10%20%20-ping-fang-de-shu-xing-%20DP.html</link><description>### 跟 Nachia DP 博客学DP-10  平方的树形 DP、$O(NK)$  的树形 DP

假设目标有根树的顶点数为 $n$，每个顶点有大小为 $1$ 的数据，数据的大小大概等同于子树中处理过的节点数目

父亲节点要把所有子节点的数据 **两两合并**

大致两种情形：

1. 经典的 $O(N^2)$ 树形 DP （“树上背包”的雏形）

   - **合并规则**：大小为 $a$ 和 $b$ 的两个包合并，代价是 $a \times b$，合并后新包大小是 $a + b$ （*其实有点像之前讲的卷积对吧哈哈*）

   - 为什么复杂度是 $O(N^2)$ ？

     表面上看，如果每个节点都把子树大小乘一遍，像是 $O(N^3)$.   但这里有个经典的**“子树大小平方和”**证明：

     - 代价 $a \times b$ 等价于 “在合并时，把 $a$ 包的点和 $b$ 包的每个点配对一次”.   整个树中，**任意两个节点只在他们的 LCA（最近公共祖先）处被配对一次**.   所以总配对数是 $\binom{N}{2}$，也就是 $O(N^2)$.   这就是 **平方树 DP** 名字的由来～

2. $O(NK)$ 的树形 DP（带容量限制的背包）

   - **合并规则**：合并代价仍然是 $a\times b$，但数据大小被**截断**了——大小是 $min(a+b,K)$.   这里的 $K$ 是一个常数，常见的有背包容量上限

   - 为什么复杂度是 $O(NK)$ ？

     因为大小超过 $K$ 的部分被“扔掉”了，每个节点上的数据大小最多只有 $K$

     这里我们可以用一个类似的分析，和上面一样；或者用更加精妙的分析，如果我们把大小小于 $K$ 的包称作 “小的”，大于的称作 “大的”，然后分类讨论：

     - **小与大合并**

       代价显然是 $size(small) \cdot K$

       每个元素作为 小-大 合并中的小元素被选中的次数最多是 1 次，因为一个小包和大包合并后总会变成一个大包.   此外，由于原始数量为 $N$，所以 $\sum(small)$ 总是比 $N$ 小的；于是我们有成本总和为 $O(NK)$

     - **大与大合并**

       两个大包合并一次的代价是：$K \times K = K ^ 2$

       如果一共有 $x$ 次合并，那么至少需要 $x + 1$ 个包，大包的总数最大是 $N / K$ 个，因为大包至少是 $K$ 大小的，而一共只有 $N$ 个元素

       所以合并次数 $\le N / K$，总代价 $\le (N/K) \times K^2 = NK$，是 $O(NK)$ 的

     - **小与小合并**

       *这个可能是最难讨论的一部分 :(*

       两个小包合并的时候，新包大小至多小于 $2K$（两个小于 $K$ 的合并，结果肯定小于 $2K$，之后如果继续让新包参与合并，那么就不在小小合并的讨论范畴了，因为其已经变成了大包）

       小小合并显然代价是 $O(size^2)$

       而所有包的大小要满足 $\sum_{i=1}^m size_i = N$，如果假设包一共有 $m$ 个，总代价就是 $O(\sum_{i=1}^m size_i^2)$

       那么这个总代价什么时候最大呢？显然是 $size$ 越大越好，于是也刚好是 $O(NK)$ ！

     这样以来，我们就证明了其复杂度是 $O(NK)$ 的

#### 例题

##### $O(N^2)$ 的 树形 DP

&gt; [!note]
&gt;
&gt; 给定一颗有 $N$ 个点的树，标号从 $1$ 开始，求包含顶点 $1$ 且顶点数（树大小）刚好是 $K$ 的有根树个数
&gt;
&gt; **[约束]**
&gt;
&gt; - $1 \le K \le N \le 3000$

显然使用标准形状存储一维是不够的，于是扩展维度： $dp[v][i]=$ 以顶点 $v$ 为根的、顶点数是 $i$ 的有根树个数

于是可以写出一个 树上背包 的代码：

```cpp
auto Mainsol = [](){
  INT(N, K);
	v&lt;v&lt;I&gt;&gt; e(N);
	rep(N - 1) {
		INT(x, y); -- x, -- y;
		e[x].pb(y), e[y].pb(x);
	}
	v&lt;v&lt;ll&gt;&gt; f(N, v&lt;ll&gt;(N + 1, 0));
	v&lt;I&gt; siz(N, 0);
	auto dfs = [&amp;](auto&amp;&amp;dfs, int u = 0, int fa = -1) -&gt; void {
		siz[u] = 1;
		f[u][1] = 1; // 只选自己
		each(v, e[u]) if(v != fa) {
			dfs(dfs, v, u);
			::v&lt;ll&gt; now(siz[u] + siz[v] + 1, 0);
			rep(i, siz[u] + 1) rep(j, siz[v] + 1) now[i + j] += f[u][i] * f[v][j];
			siz[u] += siz[v];
			f[u] = move(now);
		}
		f[u][0] = 1;
	};
	dfs(dfs, 0);
	out(f[0][K]);
};
```

##### $O(NK)$ 的树上 DP

&gt; [!note]
&gt;
&gt; 给定一颗有 $N$ 个点的树，标号从 $1$ 开始，求包含顶点 $1$ 且顶点数（树大小）刚好是 $K$ 的有根树个数
&gt;
&gt; **[约束]**
&gt;
&gt; - $1 \le N \le 10^5,\ \ 1\le K \le 500$

其实可以写出一个长得很像的代码，证明其复杂度是 $O(NK)$ 而不是 $O(NK^2)$ 的方法，其实在前面：

```cpp
auto Mainsol = [](){
  INT(N, K);
	v&lt;v&lt;I&gt;&gt; e(N);
	rep(N - 1) {
		INT(x, y); -- x, -- y;
		e[x].pb(y), e[y].pb(x);
	}
	v&lt;v&lt;ll&gt;&gt; f(N, v&lt;ll&gt;(N + 1, 0));
	v&lt;I&gt; siz(N, 0);
	auto dfs = [&amp;](auto&amp;&amp;dfs, int u = 0, int fa = -1) -&gt; void {
		siz[u] = 1;
		f[u][1] = 1; // 只选自己
		each(v, e[u]) if(v != fa) {
			dfs(dfs, v, u);
			::v&lt;ll&gt; now(min(siz[u] + siz[v], K) + 1, 0);
			rep(i, min(siz[u], K) + 1) rep(j, min(siz[v], K) + 1) 
                now[i + j] += f[u][i] * f[v][j];
			siz[u] += siz[v];
      		chmin(siz[u], K);
			f[u] = move(now);
      		f[u].resize(min(siz[u], K) + 1, 0);
		}
		f[u][0] = 1;
	};
	dfs(dfs, 0);
	out(f[0][K]);
};
```

这个代码之中其实就是多了一些剪枝，但是却可以让其在不同的约束下有不同的复杂度表现！

还有一个情况的树上 DP，请期待之后的文章！

#### 例题与其他阅读

##### 例题

###### EAZY

[P2014 [CTSC1997] 选课 - 洛谷](https://www.luogu.com.cn/problem/P2014)

$O(N^2)$ 或 $O(NK)$ 的树上 DP，与模板几乎相同！

```cpp
auto Mainsol = [](){
  INT(N, M);
  v&lt;v&lt;I&gt;&gt; e(N + 1);
  v&lt;I&gt; s(N + 1, 0);
  rep(i, N) {
  	INT(x); in(s[i + 1]);
  	e[x].pb(i + 1); e[i + 1].pb(x);
  }
  v&lt;v&lt;ll&gt;&gt; f(N + 1, v&lt;ll&gt;(N + 2, 0));
  v&lt;I&gt; siz(N + 1, 0);
  auto dfs = [&amp;](auto&amp;&amp;dfs, int u, int fa = -1) -&gt; void {
  	siz[u] = 1;
  	f[u][0] = 0;
  	f[u][1] = s[u];
  	each(v, e[u]) if(v != fa) {
  		dfs(dfs, v, u);
  		::v&lt;ll&gt; now(siz[u] + siz[v] + 1, 0);
  		rep(i, 1, siz[u] + 1) rep(j, siz[v] + 1) {
  			chmax(now[i + j], f[u][i] + f[v][j]);
  		}
  		siz[u] += siz[v];
  		rep(i, sz(now)) {
  			chmax(f[u][i], now[i]); 
  		}
  	}
  };
  dfs(dfs, 0);
  out(f[0][M + 1]);
};
```

[P2015 二叉苹果树 - 洛谷](https://www.luogu.com.cn/problem/P2015)

```cpp
auto Mainsol = [](){
  INT(N, Q);
  v&lt;v&lt;pll&gt;&gt; e(N);
  ll SS = 0;
  rep(N - 1) {
  	INT(x, y, v); -- x, -- y;
  	e[x].pb({y, v}), e[y].pb({x, v});
  	SS += v;
  }
  // dbg(SS);
  v&lt;v&lt;ll&gt;&gt; f(N, v&lt;ll&gt;(N + 100, 0));
  v&lt;I&gt; siz(N, 0);
  auto dfs = [&amp;](auto&amp;&amp;dfs, int u, int fa = -1) -&gt; void {
  	siz[u] = 0;
  	f[u][0] = 0;
  	each(V, e[u]) if(V.fst != fa) {
  		auto [v, w] = V;
  		dfs(dfs, v, u);
  		::v&lt;ll&gt; now(min(siz[u] + siz[v] + 1, Q) + 1, 0);
  		rep(i, min(siz[u], Q) + 1) rep(j, min(siz[v], Q) + 1) {
  			if(i + j &lt;= Q) chmax(now[i], f[u][i]);
  			if(i + j &lt; Q) chmax(now[i + j + 1], f[u][i] + f[v][j] + w);
  		}
  		siz[u] += siz[v] + 1;
  		rep(i, min(siz[u] + siz[v] + 1, Q) + 1)
  			f[u][i] = now[i];
  	}
  };
  dfs(dfs, 0);
  // dbg(f);
  out(f[0][Q]);
};

```

###### MID

[P - うなぎ](https://atcoder.jp/contests/tdpc/tasks/tdpc_eel)

树上 $O(NK)$ DP + NFA DP

&gt; [!tip]
&gt;
&gt; **[提示1]**：考虑点的度数
&gt;
&gt; **[提示2]**：考虑点不同度数之间的转移

```cpp
auto Mainsol = [](){
  INT(N, K);
  v&lt;v&lt;I&gt;&gt; e(N);
  rep(N - 1) {
  	INT(x, y); -- x, -- y;
  	e[x].pb(y), e[y].pb(x);
  }
  v&lt;v&lt;v&lt;mm&gt;&gt;&gt; f(N, v&lt;v&lt;mm&gt;&gt;(K + 1, v&lt;mm&gt;(3, 0))); // f[N][size][0/1/2]
  v&lt;I&gt; siz(N, 0);
  auto dfs = [&amp;](auto&amp;&amp;dfs, int u, int fa = -1) -&gt; void {
  	siz[u] = 1;
  	f[u][0][0] = 1;
  	f[u][0][1] = 1;
  	f[u][0][2] = 0;
  	each(v, e[u]) if(v != fa) {
  		dfs(dfs, v, u);
  		int ssz = min(siz[u] + siz[v], K);
  		::v&lt;::v&lt;mm&gt;&gt; now(K + 1, ::v&lt;mm&gt;(3, 0));
  		rep(i, min(siz[u], K) + 1) rep(j, min(siz[v], K) + 1) if(i + j &lt;= K) {
  			now[i + j][0] += f[u][i][0] * (f[v][j][0] + f[v][j][2]);
  			now[i + j][1] += f[u][i][1] * (f[v][j][0] + f[v][j][2]);
  			now[i + j][2] += f[u][i][2] * (f[v][j][0] + f[v][j][2]);
  			now[i + j][1] += f[u][i][0] * f[v][j][1];
  			if(i + j + 1 &lt;= K) 
  				now[i + j + 1][2] += f[u][i][1] * f[v][j][1];
  		}
  		siz[u] += siz[v];
  		rep(i, min(siz[u], K) + 1) {
  			f[u][i][0] = now[i][0];
  			f[u][i][1] = now[i][1];
  			f[u][i][2] = now[i][2];
  		}
  	}
  };
  dfs(dfs, 0);
  out((f[0][K][0] + f[0][K][2]).x);
};
```
[CF Hello 2019 G](https://codeforces.com/contest/1097/problem/G)

###### HARD

[D - Zigzag Tree](https://atcoder.jp/contests/arc130/tasks/arc130_d)

##### 阅读

[树与计算量 前篇 〜O(N^2)与 O(NK)的树形 DP〜 - 如果有人问你“你是骗子吗”，请回答“YES”的博客 --- 木と計算量 前編 〜O(N^2)とO(NK)の木DP〜 - あなたは嘘つきですかと聞かれたら「YES」と答えるブログ](https://snuke.hatenablog.com/entry/2019/01/15/211812)

。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-10%20%20-ping-fang-de-shu-xing-%20DP.html</guid><pubDate>Mon, 29 Jun 2026 13:53:53 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-9  树形 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-9%20%20-shu-xing-%20DP.html</link><description>### 跟 Nachia DP 博客学DP-9 : 树形 DP

遇到关于有根树 $T$ 的问题时，设定以下子问题

&gt; [!tip]
&gt;
&gt; $dp[v] =$ （关于顶点 $v$ 问题的解）

该问题通常涉及顶点 $v$ 的子树问题，这里顶点 $v$ 的子树是指顶点集合为 $T$ 中 $v$ 的后代，且根为 $v$ 的 $T$ 的有根子树

仅从叶子向根方向（自底向上）或从根向叶子方向（自顶向下）之一扫描有根树的 DP 被称为树形 DP

树形 DP 有非常非常多变种，该文章仅讨论最简单的一类，其余的会在后续文章出现

对于树形 DP 入门，我更倾向于在例题的辅助下讲解，其本身并不具有什么可以讲的东西（与正常的 动态规划 相区分）

#### 讲解

##### 例题1:  [AtCoder EDPC:  P - Independent Set](https://atcoder.jp/contests/dp/tasks/dp_p)

[AtCoder EDPC:  P - Independent Set](https://atcoder.jp/contests/dp/tasks/dp_p)

&gt; [!note]
&gt;
&gt; 一颗有 $N$ 个节点的树，编号从 $1,2,\dots,N$ ，对于每个 $i = 1,2,\dots,N-1$，第 $i$ 条边连接 $x_i$ 与 $y_i$ 两个点.   太郎打算把**每个点**染成**白**或**黑**色，相邻两个点**不能**都是黑色的.
&gt;
&gt; 找到染色方案数，对 $10^9 + 7$ 取模.
&gt;
&gt; **[约束]**
&gt;
&gt; - $1 \le N \le 10^5$
&gt; - $1 \le x_i,y_i \le N$

这道题使用一个维度显然是不足以存储所有的元素的，因为该点是白还是黑色是影响其后续递推的，于是我们重新设 DP 表：$dp[v][color]=$ 以点 $v$ 为根且点 $v$ 是 $color$ 颜色时候的方案数，其中 $color = 0/1$

如果当前点是白色的话，其孩子可以是白或黑色的；如果当前点是黑色的话，其孩子仅可以是白色的，于是我们就可以写出来：

- $dp[u][white] = \prod_{v \in e[u]} (dp[v][white] + dp[v][black])$
- $dp[u][black] = \prod_{v \in e[u]} (dp[v][white])$

于是我们可以写出一个代码：

```cpp
auto Mainsol = [](){
  INT(N);
  v&lt;v&lt;I&gt;&gt; e(N);
  rep(N - 1) {
  	INT(x, y); -- x, -- y;
  	e[x].pb(y); e[y].pb(x);
  }
  v&lt;a&lt;mm ,2&gt;&gt; f(N, {0, 0});
  auto dfs = [&amp;](auto&amp;&amp;dfs, int u, int fa = -1) -&gt; void {
  	f[u][0] = f[u][1] = 1;
  	each(v, e[u]) {
  		if(v == fa) continue;
  		dfs(dfs, v, u);
  		f[u][0] *= f[v][0] + f[v][1];
  		f[u][1] *= f[v][0];
  	}
  };
  dfs(dfs, 0);
  out((f[0][0] + f[0][1]).x);
};
```

相同的题： [AtCoder ABC036: D - 涂色画](https://atcoder.jp/contests/abc036/tasks/abc036_d)

##### 例题2: [AtCoder TDPC:  N - 木](https://atcoder.jp/contests/tdpc/tasks/tdpc_tree)

[AtCoder TDPC:  N - 木](https://atcoder.jp/contests/tdpc/tasks/tdpc_tree)

&gt; [!note]
&gt;
&gt; 给定你 $N - 1$ 条边，你可以以任意顺序选择边被绘画的先后，但是每一次绘画后，你要保证所有边是联通的，求出绘画方案，对 $10^9 + 7$ 取模
&gt;
&gt; **[约束]**
&gt;
&gt; - $2 \le N \le 1000$
&gt; - $1 \le a_i,b_i \le N$

如果我们假设第一条绘画的边上有一个点是整体的根，那么对于所有的子树而言，第一次遇到一个新节点时，绘画的边必须包含子树的根，这样才能满足联通，但是一旦联通后，后续的所有边的选择顺序无所谓

所以我们知道，当边 $e_v$ 是连接 $v$ 和其父亲的边，我们必须把 $e_v$ 放在所有其他 $v$ 的子树中的边之前，而放在前面的概率是 $P(e_v) = \frac{1}{size[v]}$

那么所有非根节点的情况都满足的概率就是 ： $P = \prod_{v=2}^N \frac{1}{size[v]}$

$N - 1$ 条边的所有排列法就是 $(N - 1) !$，那么合法方案数就是 $P \cdot (N-1)! = \frac{(N-1)!}{\prod_{v=2}^N {size[v]}}$

于是我们先枚举以每个点为根的情况，在每个点为根的时候，预处理出其余点在当前情况下的子树大小即可，复杂度是 $O(N^2)$ 的

这道题的其实的动态规划其实在求子树大小！

- $size[u]=$ 以 $u$ 为根的子树的大小

显然有： $size[u] = 1 + \sum_{v \in to[u]} size[v]$

但是我们枚举每一个点之后会把相同的点计算两次贡献，所以答案要除以 $2$，这样就可以了，代码如下：

```cpp
auto Mainsol = [](){
  INT(N); 
  Comb C(N * N); // 预处理阶乘
  v&lt;v&lt;I&gt;&gt; e(N);
  rep(N - 1) {
  	INT(x, y); -- x, -- y;
  	e[x].pb(y), e[y].pb(x);
  }
  mm ans = 0, cons = C.fac(N - 1);
  dbg(cons.x);
  rep(root, N) {
  	v&lt;ll&gt; f(N, 0); // size
	auto dfs = [&amp;](auto&amp;&amp;dfs, int u, int fa = -1) -&gt; void{
		f[u] = 1;
		each(v, e[u]) if(v != fa) {
			dfs(dfs, v, u);
			f[u] += f[v];
		}
	};
	dfs(dfs, root);
	mm now = cons;
	rep(i, N) if(i != root) now *= ((mm)f[i]).inv();
  	ans += now;
  }
  out((ans/2).x);
};
```

其他的树形 DP，请期待后续的文章，谢谢！

#### 拓展阅读与例题

https://yukicoder.me/problems/no/417

http://tdpc.contest.atcoder.jp/tasks/tdpc_eel

http://codeforces.com/contest/855/problem/C

##### 拓展阅读

[树形 DP 教程 - Codeforces --- DP on Trees Tutorial - Codeforces](https://codeforces.com/blog/entry/20935)

[竞技编程中的树形 DP 问题总结 - 哈马扬哈马扬哈马扬 --- 競技プログラミングにおける木DP問題まとめ - はまやんはまやんはまやん](https://blog.hamayanhamayan.com/entry/2017/06/19/161741)。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-9%20%20-shu-xing-%20DP.html</guid><pubDate>Sun, 28 Jun 2026 16:11:30 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-7&amp;8:  分发型 接收型 DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-7%268-%20%20-fen-fa-xing-%20-jie-shou-xing-%20DP.html</link><description>### 跟 Nachia DP 博客学DP-7&amp;8  分发型 接收型 DP

Nachia 原文： [DP 的俗称 | Mathenachia --- DP の俗称 | Mathenachia](https://www.mathenachia.blog/dp/)

&gt; [!tip]
&gt;
&gt; **[接收型 DP]** ：将尚未完成计算的区域作为当前状态，以已完成区域的结果来计算当前状态；按照递推式直接实现的 DP
&gt;
&gt; **[分发型 DP]**：将已完成计算的区域作为当前状态，来计算其对尚未完成区域的贡献；基于“当前状态的下一个可能状态是什么？”这一思路设计的 DP，直接实现的话，往往是分发型 DP

背包问题通常可以用上列两种情况来实现，所以很多人认为：任何 DP 问题都能在这两类 DP 处理方式中轻松转换.

下面我们仅考虑像 $dp[i]$ 这样只有一个下标的 DP，对于多个下标比如 $dp[i][j]$ 这样的，可以通过编码为 $dp[i \times n + j]$ 等方式来规约，如果我们把编码后的一对表示为 $\langle i, j\rangle$ 这样的形式，比如 $dp[\langle i, j\rangle]$

为了计算 $dp[i]$ 的值而需要 $dp[j]$ 的值时，考虑一条边指向 $j \to i$ 图，需要注意的是，根据 DP 的定义，$j$ 和 $i$ 未必是大小关系链接

如果设顶点 $i$ 的入边所连接的顶点集合记为 $\delta_{-}(i)$；同样地，顶点 $j$ 的出边所连接的顶点集合记为 $\delta_{+}(j)$.   也就是说，要计算 $dp[i]$，需要先对每个 $j \in \delta_{-}(i)$ 计算出 $dp[j]$ （充分条件）；反之，当 $dp[j]$ 计算完成后，对于每一个 $i \in \delta_{+}(j)$ 而言，就相当于获得了用于计算 $dp[i]$ 的其中一项信息（必要条件）。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-7%268-%20%20-fen-fa-xing-%20-jie-shou-xing-%20DP.html</guid><pubDate>Sat, 27 Jun 2026 16:38:53 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-6 : SOS DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-6%20-%20SOS%20DP.html</link><description>### 跟 Nachia DP 博客学DP-6 : SOS DP

Nachia 原文： [DP 的俗称 | Mathenachia --- DP の俗称 | Mathenachia](https://www.mathenachia.blog/dp/)

也被称作 $3^n$ 的 DP （$O(3^n)$ 的 DP、子集 DP），SOS DP 的全称是 Sum over Subsets！

&gt; [!tip]
&gt;
&gt; 在集合 $A$ 上的 Bit DP 中，在计算 $dp[B] \ (B \subset A)$ 时需要枚举并引用满足 $C \subsetneq B$ 的 $C$ 时，总的引用次数大约是 $3^{|A|}$ 
&gt;
&gt; 这里是证明：
&gt;
&gt; &gt;
&gt; &gt; 对于一个元素 $x \in A$，对于一个集合对 $(B, C)$ 满足 $(C \subsetneq B \subset A)$，这个 $x$ 只可能是下面3种互斥状态中的一个：
&gt; &gt;
&gt; &gt; 1. $x \in C$：因为 $C \subsetneq B$，所以一定有 $x \in B$
&gt; &gt; 2. $x \notin C, x \in B$：只在 $B$ 中
&gt; &gt; 3. $x \notin B$：不在 $B$ 中，那么肯定不在 $C$ 中
&gt; &gt;
&gt; &gt; 所以对于 $|A|$ 个元素中的每一个，都有可能是这三个状态中的一个，所以总的 $(B, C)$ 的配对总数就是： $3 \times 3 \times 3 \times \dots \times 3 = 3 ^ {|A|}$
&gt; &gt;
&gt; &gt; 所以总枚举数就是 $O(3 ^ n)$
&gt;
&gt; 这样的 DP 就被称为 $O(3^n) 的 DP$

例如在最小覆盖问题中，当需要从一个子集转移到另一个子集，且转移方向仅限于集合变大的时候，就可以通过 $O(3^n)$ 的算法解决.

#### 例题

##### [**AtCoder ABC187 F - Close Group**](https://atcoder.jp/contests/abc187/tasks/abc187_f)

&gt; [!note]
&gt;
&gt; 给定一个 $N$ 个顶点与 $M$ 条边的简单无向图，标号 $1,2,\dots,N$，第 $i$ 个边连接顶点 $A_i$ 与 $B_i$
&gt;
&gt; 删除若干（可能是0）条边后，使得**下面条件成立的联通块（团）**的最小数量：
&gt;
&gt; - 对于任意一对顶点 $(a,b)$ 满足 $1\le a \lt b\le N$，如果顶点 $a$ 与 $b$ 在同一个联通块里，那么这两个点必定有一条**直接连接**这两个顶点的边
&gt;
&gt; **[约束]**
&gt;
&gt; - $1 \le N \le 18$
&gt; - $0 \le M \le \frac{N(N-1)}{2}$
&gt; - $1 \le A_i \lt B_i \le N$
&gt; - $(A_i,B_i) \neq (A_j, B_j)$ for $i \neq j$

我们定义 $F(S)$ 为顶点集合 $S$ **完全分割成若干个完全图**，所需要的最少组数，考虑如何求 $F(S)$ :

1. 如果图 $G[S]$ 是完全图，$F(S) = 1$
2. 如果图 $G[S]$ 不是完全图，那么 $F(S) = \min_{\varnothing \neq T \subsetneq S} (F(T) + F(S \setminus T))$

而我们要求 $F(V)$，其中 $V$ 是全集，也就是全部 $N$ 个顶点所代表的集合.

这里就涉及到 SOS DP 的关键：

&gt; [!note]
&gt;
&gt; 对于一个元素 $x \in V$ 来说，他在每一对 $(T, S)$ 中只有三种情况：
&gt;
&gt; 1. $x \in T$
&gt; 2. $x\notin T, x\in S$
&gt; 3. $x \notin S$
&gt;
&gt; 所以这个东西显然是可以 $O(3^N)$ DP 的！

而这个东西为什么可以 $O(3 ^ N)$，关键在于计算的顺序，因为我们总是按照 **集合从小到大** 的顺序去计算.

而如果不去这样算呢？我们可以计算一下，也就是 $T$ 与 $S$ 之间没有子集关系，那么就是 $O(2^N \cdot 2^N) = O(4^N)$，所以这里我们注意到了 $T$ 与 $S$ 的从属关系，于是有优化成 $O(3^N)$

所以代码思路大概就是这样的：

1. 先找出所有的完全图：标记哪些子集可以作为一个团
2. 用 SOS DP 来求 $F(V)$

```cpp
void Mainsol() {
	INT(N, M);
	v&lt;v&lt;I&gt;&gt; e(N);
	rep(M) {
		INT(a, b); -- a; -- b;
		e[a].pb(b); e[b].pb(a);
	}
	v&lt;I&gt; f(1 &lt;&lt; N, INF); f[0] = 1;

	rep(i, N) rep(j, 1 &lt;&lt; N) { // 计算所有的团
		if(f[j] != 1) continue;
		bool is_tuan = 1;
		rep(k, N) if(j &amp; (1 &lt;&lt; k)) {
			bool connect = 0;
			each(v, e[i]) if(v == k) {connect = 1; break;}
			if(!connect) {is_tuan = 0; break;}
		}
		if(is_tuan) f[j | (1 &lt;&lt; i)] = 1;
	}

	rep(i, 1, 1 &lt;&lt; N)
		for(int j = (i - 1) &amp; i; j; j = (j - 1) &amp; i) // 初始化 j 为 i的真子集 中最大的集合，循环结束是 j 转移为比 j 小的 i 的子集
			chmin(f[i], f[i ^ j] + f[j]);

	out(f[(1 &lt;&lt; N) - 1]);
}
```

请记住这个 SOS DP 的这个经典结构，也就是找其真子集的过程：

```cpp
rep(i, 1, 1 &lt;&lt; N)
		for(int j = (i - 1) &amp; i; j; j = (j - 1) &amp; i)
```

当然还有一种等价的更简洁写法：

```cpp
rep(i, 1, 1 &lt;&lt; N) for(int j = i; -- j &amp;= i; )
```

```cpp
#define rep(i, a, b) for(int i = (a); i &lt; (b); i ++)
```

##### [AtCoder EDPC U - Grouping](https://atcoder.jp/contests/dp/tasks/dp_u)

&gt; [!note]
&gt;
&gt; $N$ 个兔子，编号 $1, 2, \dots, N$
&gt;
&gt; 对于每一个点对 $1 \le i, j \le N$，兔子 $i, j$ 的适配点数就是 $a_{i,j}$，其中 $a_{i,i}=0, \ a_{i,j}=a_{j,i}$
&gt;
&gt; 他要把这 $N$ 个兔子划分成若干个组合，每一个兔子必须刚好分入一个组，一个组的价值是每一对 $1\le i, j \le N$ 其中 $i, j$ 兔子在同一个组，$val(S) = \sum a_{i,j}$ 
&gt;
&gt; **[约束]**
&gt;
&gt; - $1 \le N \le 16$
&gt; - $|a_{i,j}| \le 10^9$
&gt; -  $a_{i,i}=0, \ a_{i,j}=a_{j,i}$

这道题其实和上面那个是类似的，对于一个集合 $S$，其原本会有一个 $val(S)$，然后我们考虑能不能用其子集 $T$ 和这个差集 $S \setminus T$ 之和来更新即可，所以显然可以写出转移方程：

- $dp[S] = \max_{\varnothing \neq T \subsetneq S} (dp[T] + dp[S \setminus T])$

于是我们就经典两步走：

1. 先更新出每一个 $0 \le bit  \le (1 \lt \lt N)$ 的初始 $val(bit)$，这一部分复杂度是 $(O(2^N \cdot N^2))$
2. 然后我们用 SOS DP 来考虑更新这个转移方程，复杂度是 $O(3 ^ N)$，论证就不说辣！

```cpp
void Mainsol() {
	INT(N);
	VV(ll, A, N, N);
	v&lt;ll&gt; f(1 &lt;&lt; N), val(1 &lt;&lt; N, 0);
	rep(bit, 1 &lt;&lt; N) rep(i, N) rep(j, i + 1, N) if(bit &amp; (1 &lt;&lt; i)) if(bit &amp; (1 &lt;&lt; j))
		val[bit] += A[i][j];
	rep(i, 1, 1 &lt;&lt; N) {
		f[i] = val[i];
		for(ll j = i; -- j &amp;= i; ) {
			chmax(f[i], f[i ^ j] + val[j]);
		}
	}
	out(f[(1 &lt;&lt; N) - 1]);
}
```

#### 拓展阅读与例题

##### EAZY

TODO

##### Middle

TODO

##### 拓展阅读

[SOS Dynamic Programming [Tutorial\] - Codeforces](https://codeforces.com/blog/entry/45223)

[SOS-DP 详解 | Eysiking](https://lineagestar.github.io/2025/08/17/Algorithm/SOSDP/index.html)









。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-6%20-%20SOS%20DP.html</guid><pubDate>Tue, 23 Jun 2026 18:01:25 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-5 : Bit DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-5%20-%20Bit%20DP.html</link><description>### 跟 Nachia DP 博客学DP-5 : Bit DP

Nachia 原文： [DP 的俗称 | Mathenachia --- DP の俗称 | Mathenachia](https://www.mathenachia.blog/dp/)

状态压缩 DP 是一种常用于需要从 $n$ 个元素的排列（共有 $n!$ 中可能性）中寻找最优解的 DP .  常用的框架如下：

&gt; [!tip]
&gt;
&gt; $dp[S] :=$ 对于全集 $U$ 的子集 $S$，在**顺序最佳化**后， $S$ 的最小成本
&gt;
&gt; 其中 $(S \subset U)$，$U$ 指的是 $n$ 个元素的排列.

常用的递推关系如下：

- $dp[S] = \min_{i\in S} (dp[S \setminus \set{i}] + cost(S \setminus \set{i}, i))$

该递推式子指的是：对于集合 $S$，我们希望计算其中元素经过顺序优化后的成本 $dp[S]$，但需要根据该顺序中 **最后的元素** 是什么来分类讨论：

- 当 $S$ 的最后一个元素是 $i$ 时，关于 $S$ 中除去最后一个 $i$ 的子集的最优解是 $dp[S -\setminus\set{i}]$ .  如果将 $S \setminus \set{i}$ 加上 $i$ 的成本记为 $cost(S \setminus \set i, i)$，那么最终的最小成本就是 $dp[S \setminus \set i] + cost(S \setminus \set i, i)$

之后，对于所有 $i \in S$ 尝试这个情况，然后采用成本最小的方案即可.  于是我们有：

&gt; [!tip]
&gt;
&gt; **[转移方程]**： $dp[S] = \min_{i\in S} (dp[S \setminus \set{i}] + cost(S \setminus \set{i}, i))$
&gt;
&gt; **[初始值]**：$dp[0]=0$
&gt;
&gt; **[最终的最小成本（解）]**：$dp[(1&lt;&lt;n)-1]$

状压 DP 框架所解决的一个经典问题便是著名的 TSP 问题 （*Traveling salesman problem*, 旅行商问题）。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-5%20-%20Bit%20DP.html</guid><pubDate>Tue, 23 Jun 2026 15:14:07 +0000</pubDate></item><item><title>OCAML CS3110 - Unit 3 Practice</title><link>https://SistineFibel-2006.github.io/post/OCAML%20CS3110%20-%20Unit%203%20Practice.html</link><description>### OCAML CS3110 - Unit 3 Practice

```ocaml
let l1 = [1; 2; 3; 4; 5]
let l2 = 1 :: 2 :: 3 :: 4 :: 5 :: []
let l3 = [1] @ [2; 3; 4] @ [5]

let rec product (xs : 'a list) : int = 
  match xs with
  | [] -&gt; 1
  | h :: t -&gt; h * product t

let rec concat (xs : string list) : string = 
  match xs with
  | [] -&gt; ''
  | h :: t -&gt; h ^ concat t

(* patten match *)

let rec pat1 = function
| [] -&gt; false
| h :: _ -&gt; h = 'bigred'

let pat2 = function
| _ :: _ :: [] | _ :: _ :: _ :: _ :: [] -&gt; true
| _ -&gt; false

let pat3 = function
| x :: y :: [] -&gt; x = y
| _ -&gt; false

(* library *)

let fiveth (xs : int list) : int = 
  if List.length xs &gt;= 5 then List.nth xs 4 else 0

let downlist (xs : int list) : int list = 
  List.sort Stdlib.compare xs |&gt; List.rev 

(* library puzzle *)

let lastele xs = 
  List.nth (List.rev xs) 0 

let haszero (xs : int list) =
  List.exists (fun x -&gt; x = 0) xs

(* take drop *)

let rec take n xs : int list =
  match n with 
  | 0 -&gt; []
  | _ -&gt; begin
      match xs with 
      | [] -&gt; []
      | h :: t -&gt; h :: take (n - 1) t
    end

let rec drop n xs : int list =
  match n with 
  | 0 -&gt; xs
  | _ -&gt; begin
      match xs with 
      | [] -&gt; []
      | h :: t -&gt; drop (n - 1) t
    end

(* unimodal *)

let rec is_dec (xs : int list) : bool =
  match xs with
  | [] | [_] -&gt; true
  | h1 :: (h2 :: t1 as t) -&gt; h1 &gt;= h2 &amp;&amp; is_dec t

let rec is_inc_dec = function
  | [] | [_] -&gt; true
  | a :: (b :: t) as t1 -&gt; 
    if a &lt;= b then is_inc_dec t else is_dec t1  

let is_unimodal xs = is_inc_dec xs

(* powerset *)

let rec powerset (xs : int list) : int list list = 
  let ss = List.sort Stdlib.compare xs in
  match ss with
  | [] -&gt; [[]]
  | h :: t -&gt; let p = powerset t in 
    List.map (List.cons h) p @ p 

(* print int list rec *)
let rec print_int_list (xs : int list) : unit = 
  match xs with 
  | [] -&gt; ()
  | h :: t -&gt; print_endline @@ string_of_int h; print_int_list t

(* print int list iter *)

let print_int_list' xs =
  List.iter (fun x -&gt; print_endline @@ string_of_int x) xs

(* student *)

type student = {first_name : string; last_name : string; gpa : float}

let a1 : student = {first_name = 'Line'; last_name = 'BuBu'; gpa = 3.1}

let student_name stu = 
  (stu.first_name, stu.last_name)

let student_create f l g : student = 
  {first_name = f; last_name = l; gpa = g}

(* pokerecord *)

type poketype = Normal | Fire | Water
type pokemen = {name : string; hp : int; ptype : poketype}

let charizard = {name = 'charizard'; hp = 78; ptype = Fire}
let squirtle = {name = 'squirtle'; hp = 44; ptype = Water}

(* safe hd and tl *)

let safe_hd = function
  | [] -&gt; None
  | h :: _ -&gt; Some h 

let rec safe_tl = function
  | [] -&gt; None
  | _ :: t -&gt; match t with 
    | [] -&gt; failwith 'can't be trigged' 
    | h :: [] -&gt; Some h
    | _ -&gt; safe_tl t

(* pokefun *)

let rec max_hp (xs : pokemen list) : pokemen option = 
  match xs with 
  | [] -&gt; None
  | h :: t -&gt; match max_hp t with
    | None -&gt; Some h
    | Some h1 -&gt; Some (if h1.hp &gt;= h.hp then h1 else h)

(* date before *)

type date = int * int * int
let is_before (x : date) (y : date) : bool = 
  let (x1,x2,x3) = x in 
  let (y1,y2,y3) = y in 
  x1 &lt; y1 || (x1 = y1 &amp;&amp; x2 &lt; y2) || (x1 = y1 &amp;&amp; x2 = y2 &amp;&amp; x3 &lt; y3) 


(* earliest date *)

let rec earliest (xs : date list) : date option = 
  match xs with 
  | [] -&gt; None
  | h :: t -&gt; match earliest t with
    | None -&gt; Some h
    | Some h1 -&gt; Some (if is_before h1 h then h1 else h)

(* assoc list *)

let insert k v lst = (k, v) :: lst
let rec lookup k = function
| [] -&gt; None
| (k', v) :: t -&gt; if k = k' then Some v else lookup k t

let mp = insert 1 'one' (insert 2 'two' (insert 3 'three' []))
let ck2 = lookup 2 mp
let ck4 = lookup 4 mp

(* cards *)

type suit = Hearts | Spades | Clubs | Diamonds
type rank = Number of int | Ace | Jack | Queen | King 
type card = {suit : suit; rank : rank}

(* quadrant *)

type quad = I | II | III | IV 
type sign = Neg | Zero | Pos 

let sign (x : int) : sign = 
  if x = 0 then Zero else if x &gt; 0 then Pos else Neg

let quadrant (point : int * int) : quad option = 
  let (x, y) = point in 
  match sign x, sign y with
  | Pos, Pos -&gt; Some I 
  | Neg, Pos -&gt; Some II 
  | Neg, Neg -&gt; Some III 
  | Pos, Neg -&gt; Some IV 
  | _ -&gt; None 

let quadrant_when (point : int * int) : quad option = 
  let (x, y) = point in
  match x, y  with 
  | x, y when x &gt; 0 &amp;&amp; y &gt; 0 -&gt; Some I
  | x, y when x &lt; 0 &amp;&amp; y &gt; 0 -&gt; Some II 
  | x, y when x &lt; 0 &amp;&amp; y &lt; 0 -&gt; Some III 
  | x, y when x &gt; 0 &amp;&amp; y &lt; 0 -&gt; Some IV 
  | _ -&gt; None 

(* depth *)

type 'a tree = Nil | Node of 'a * 'a tree * 'a tree

let rec depth (tree : 'a tree) : int = 
match tree with 
| Nil -&gt; 0
| Node (_, l, r) -&gt; max (1 + depth l) (1 + depth r)

(* shape *)
let rec shape (a : 'a tree) (b : 'a tree) : bool = 
match a, b with
| Nil, Nil -&gt; true
| Node (_, l1, r1) , Node (_, l2, r2) -&gt; shape l1 l2 &amp;&amp; shape r1 r2
| _ -&gt; false

(* list max exn *)
let rec list_max = function
| [] -&gt; failwith 'empty'
| h :: [] -&gt; h
| h :: t -&gt; max h @@ list_max t

(* list max exn string *)
let list_max_string (xs : int list) : string = 
match xs with 
| [] -&gt; 'empty'
| _ -&gt; string_of_int @@ list_max xs

(* is_bst *)
let is_bst : ('a * 'b) tree -&gt; bool = fun xs -&gt; 
  failwith 'TODO'
```

。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/OCAML%20CS3110%20-%20Unit%203%20Practice.html</guid><pubDate>Tue, 23 Jun 2026 09:49:07 +0000</pubDate></item><item><title>P16231 [蓝桥杯 2026 省 A] 基因研究 **蓝 -- 自动机DP</title><link>https://SistineFibel-2006.github.io/post/P16231%20%5B-lan-qiao-bei-%202026%20-sheng-%20A%5D%20-ji-yin-yan-jiu-%20---lan-%20--%20-zi-dong-ji-DP.html</link><description>### P16231 [蓝桥杯 2026 省 A] 基因研究 solution

### 题目分析

通过观察性质，我们会发现这是比较典型的**自动机DP**，具体如何发现的呢？

这是因为，对于一个串来说，在他的后面添加新的字符之后能产生的合法新串与不合法新串，与其所在的位置无关，换句话说：**线性时不变转移**。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/P16231%20%5B-lan-qiao-bei-%202026%20-sheng-%20A%5D%20-ji-yin-yan-jiu-%20---lan-%20--%20-zi-dong-ji-DP.html</guid><pubDate>Fri, 05 Jun 2026 05:27:06 +0000</pubDate></item><item><title>CFR1098D - 1900</title><link>https://SistineFibel-2006.github.io/post/CFR1098D%20-%201900.html</link><description>### CFR1098-D

[Problem - D - Codeforces](https://codeforces.com/contest/2228/problem/D)

&gt; [!warning]
&gt;
&gt; **警钟长鸣** ：： 
&gt;
&gt; `set` 容器读入后遍历做离散化复杂度 $&gt; &gt; $   `sort(), unique()`

&lt;img width='300' height='98' alt='Image' src='https://github.com/user-attachments/assets/2a7abdb9-f0bd-4904-b7d4-1c8c473a12b1' /&gt;

#### 题意

给你 $N$ 个格点 $(x_i, y_i)$，你可以划两个线，用一个二元组 $(k_1,k_2)$ 表示： $x = k_1 + 0.5, \ \ y = k_2 + 0.5$，这两条线会把平面内的所有点分在四个区域，四个区域的点会被染成固定的四种颜色，问共有多少中分割点的方式，当且仅当至少有一种点颜色的集合不同时，认为是不一样的分割方式.

![img](https://espresso.codeforces.com/829ca6447e07e7a11da519c9aa66bfa6828386e1.png)

##### 约束

$\sum N \le 2 \cdot 10^6, \ \ \ 1 \le x_i, y_i \le N$

#### 分析

不妨先考虑定一条竖着线的时候，横着的线可以怎么画：

&lt;img width='479' height='375' alt='Image' src='https://github.com/user-attachments/assets/723d2761-8005-410b-a860-f446c10df261' /&gt;

玩一玩就会发现，设竖线左边的最大 $y$ 为 $Lmax$，左边最小为 $Lmin$，右边同理有 $Rmax, Rmin$.   会发现能划线的合法区域是 $(max(Lmin, Rmin), min(Lmax, Rmax))$ ，但是怎么样才能画出不同集合呢？

其实就是考虑在这个合法区间内有多少个 $y$ 值出现过，于是这样这道题就结束了.

##### 书写代码

先记录每个 $x$ 的 $Xmax, Xmin$ （对应 $y$ 的最大最小）.

然后从左往右推一遍算 $Lmax, Lmin$，从右往左推一遍算 $Rmax, Rmin$.

别忘了直接扫一遍前缀和算出每一个 $y$ 有多少个 $y$ 出现过，这里是 $hasy$.

复杂度是 $O(N \cdot \log N)$，瓶颈在于离散化！

#### 代码片段

该代码无法通过， `TL`；后者可以通过！


```cpp
void Mainsol() {
	INT(N);
	v&lt;I&gt; Xmax(N + 1, -1), Xmin(N + 1, INF);
	set&lt;int&gt; st;
	v&lt;I&gt; po;
	v&lt;I&gt; hasy(N + 2, 0);
	rep(N) {
		INT(x, y);
		chmax(Xmax[x], y);
		chmin(Xmin[x], y);
		hasy[y + 1] = 1;
		st.is(x);
	}

	rep(i, 1, sz(hasy)) hasy[i] += hasy[i - 1];
	each(x, st) {
		po.pb(x);
	}
	v&lt;I&gt; Lmax(N + 1, -1), Lmin(N + 1, INF), Rmax(N + 1, -1), Rmin(N + 1, INF);
	I lsti = 0;
	each(i, po) {
		Lmax[i] = Xmax[i];
		Lmin[i] = Xmin[i];
		if(lsti) {
			chmax(Lmax[i], Lmax[lsti]);
			chmin(Lmin[i], Lmin[lsti]);
		}
		lsti = i;
	}
	auto poo = po;
	rev(po);
	lsti = 0;
	each(i, po) {
		Rmax[i] = Xmax[i];
		Rmin[i] = Xmin[i];
		if(lsti) {
			chmax(Rmax[i], Rmax[lsti]);
			chmin(Rmin[i], Rmin[lsti]);
		}
		lsti = i;
	}
 	swap(po, poo);
	ll ans = 0;
	rep(j, sz(po) - 1) {
		int i = po[j], ii = po[j + 1];
		I hi = min(Lmax[i], Rmax[ii]);
		I lo = max(Lmin[i], Rmin[ii]);
		ll tt = hasy[hi] - hasy[lo];
		dbg(j, i, ii, hi, lo, tt);
		if(hi &lt;= lo) continue;
		ans += tt;
	}
	out(ans);
}
```

```cpp
void Mainsol() {
	INT(N);
	v&lt;I&gt; Xmax(N + 1, -1), Xmin(N + 1, INF);
	v&lt;I&gt; po;
	v&lt;I&gt; ppp;
	v&lt;I&gt; hasy(N + 2, 0);
	rep(N) {
		INT(x, y);
		chmax(Xmax[x], y);
		chmin(Xmin[x], y);
		hasy[y + 1] = 1;
		po.push_back(x);
	}
	rep(i, 1, sz(hasy)) hasy[i] += hasy[i - 1];
	uniq(po);
	v&lt;I&gt; Lmax(N + 1, -1), Lmin(N + 1, INF), Rmax(N + 1, -1), Rmin(N + 1, INF);
	I lsti = 0;
	each(i, po) {
		Lmax[i] = Xmax[i];
		Lmin[i] = Xmin[i];
		if(lsti) {
			chmax(Lmax[i], Lmax[lsti]);
			chmin(Lmin[i], Lmin[lsti]);
		}
		lsti = i;
	}
	auto poo = po;
	rev(po);
	lsti = 0;
	each(i, po) {
		Rmax[i] = Xmax[i];
		Rmin[i] = Xmin[i];
		if(lsti) {
			chmax(Rmax[i], Rmax[lsti]);
			chmin(Rmin[i], Rmin[lsti]);
		}
		lsti = i;
	}
 	swap(po, poo);
	ll ans = 0;
	rep(j, sz(po) - 1) {
		int i = po[j], ii = po[j + 1];
		I hi = min(Lmax[i], Rmax[ii]);
		I lo = max(Lmin[i], Rmin[ii]);
		ll tt = hasy[hi] - hasy[lo];
		dbg(j, i, ii, hi, lo, tt);
		if(hi &lt;= lo) continue;
		ans += tt;
	}
	out(ans);
}
```

。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/CFR1098D%20-%201900.html</guid><pubDate>Thu, 04 Jun 2026 10:22:09 +0000</pubDate></item><item><title>凸数列的 min-plus 卷积</title><link>https://SistineFibel-2006.github.io/post/tu-shu-lie-de-%20min-plus%20-juan-ji.html</link><description>## 凸数列的 `min-plus` 卷积 （未完结）

Atcoder Algorithm Lectures 难度评级 4 / 10

原文： [[凸数列的 min-plus 卷积 - AtCoderInfo --- 凸数列の min-plus 畳み込み - AtCoderInfo](https://info.atcoder.jp/entry/algorithm_lectures/min_plus_convolution)](https://info.atcoder.jp/entry/algorithm_lectures/min_plus_convolution)

原解说视频链接： https://youtu.be/VnFJxcYBugU

&gt; [!tip]
&gt;
&gt; 本文是原文的翻译与扩展，具体的扩展内容是后续例题的题解与讲解，译者水平有限，可能有些许错误，还望海涵，也可与文章作者联系并提出建议与指出错误！


### 1.概述

对于两个整（实）数序列 $A=(A_0, A_1, \dots, A_N), \ \ B=(B_0, B_1, \dots, B_M)$, 由他们得到的新序列 $C = (C_0, C_1, \dots, C_{N + M})$ 满足：$C_k = \min_{i+j=k} (A_i + B_j)$  被叫做 **A与B的 min-plus** 卷积.

当两个序列其中一方有 **凸性** 的时候，我们可以高效的进行这个卷积运算，而本文只讲解最简单的情况——两者均有凸性时的运算.

### 2.预备知识

无特别需要的前置知识，只会讲解基础的动态规划.

### 3.凸数列

&gt; [!note]
&gt;
&gt; **【定义 1】**
&gt;
&gt; 对于一个数列 $A=(A_0, A_1, \dots, A_N)$ 与任意的下标 $i\in [0, N - 1)$，都有不等式 $A_{i+1}- A_i \leq A_{i + 2} - A_{i + 1}$ 成立，我们就称 $A$ 是 **下凸（Convex Below）** 的；如果都有不等式 $A_{i+1}- A_i \ge A_{i + 2} - A_{i + 1}$ 成立，我们就称 $A$ 是 **上凸（Convex Above）** 的. 
&gt;
&gt; 上凸也被叫做凹，下凸也被叫做凸.

##### 3.1. 具体例子

如果对于数列 $A$，我们画出其散点相联结的 **折线图**.

下凸的条件其实就对应了折线图中 **斜率广义单增（单调不递减）** 的性质.

这里给出几组凸数列的例子，可以对照观看:

- $A=(0,0,0,1,2,4) $是下凸的，不是上凸的.
- $B=(4,1,0,1,4)$ 是下凸的，不是上凸的.
- $C=(0,2,4,5,5,4)$ 是上凸的，不是下凸的.
- $D=(4,3,2,1,0)$ 既是下凸的，也是上凸的.
- $E=(1)$ 既是下凸的，也是上凸的.
- $F=(3,1,3,0,2)$既不是下凸的，也不是上凸的.

![img](https://img.atcoder.jp/aal-image-library/d497019e37bca8837eb1da8f54b0273f.png)

![img](https://img.atcoder.jp/aal-image-library/e9aec0a6c254784a924923b9c64f30a3.png)

### 4. 凸数列的 `min-plus` 卷积

#### 4.1. 数列的 `min-plus` 卷积

&gt; [!note]
&gt;
&gt; **【定义 2】**
&gt;
&gt; 对于两个整（实）数序列 $A=(A_0, A_1, \dots, A_N), \ \ B=(B_0, B_1, \dots, B_M)$, 由他们得到的新序列 $C = (C_0, C_1, \dots, C_{N + M})$ 满足：$ C_k = \min_{i+j=k} (A_i + B_j)$  被叫做 **A与B的 min-plus** 卷积   （**min-plus convolution**）.

例如，当 $A = (5,1,4,2,2)$ 与 $B = (1,3,2)$ 时，他们的 `min-plus` 卷积就是： $C=(6,2,4,3,3,4,4)$.

&lt;img width='329' height='119' alt='Image' src='https://github.com/user-attachments/assets/c1081116-538b-496b-a381-6c5ae2832051' /&gt;

也就是这个图片里面，斜着取每一斜线最小值. （当然对于第三个数，不取 (3,1) 位置的 4 而是取 (2, 2) 位置的 4 也是等价的，这里为了保证都先按照列优先取）

这也正是 `min-plus convolution` 用定义算的方法，复杂度显然是 $O(N \cdot M)$ 的.  根据参考文献，在一般情况下，我们认为很难实现大幅度的加速 （当 $\epsilon &gt; 0$ 时，预计无法在 $O((N \cdot M)^{1-\epsilon})$ 时间内求解）.

#### 4.2. 下凸（凸）序列的 `min-plus` 卷积

不妨设 $A, B$ 是凸序列，我们旨在证明这些序列的 `min-plus` 卷积相关性质.

&gt; [!IMPORTANT]
&gt;
&gt; **【定理 3】**
&gt;
&gt; 当 $A=(A_0,A_1,\dots,A_N), \ \  B=(B_0,B_1,\dots,B_N)$ 都是凸序列的时候，他们两个的 `min-plus` 卷积可以在 $O(N + M)$ 时间内计算.  此外，卷积得到的新序列也是凸的.

因为我们有序列的增量单调不递减，所以我们不妨设 $A_0=0, B_0=0$，如果证明在这个条件下成立，那么自然可以推广到首项不为 $0$ 的情况.

我们设 $A, B$ 的**增量序列**（也可称 **差分序列**）是 $a, b$.  也就是 $a_i=A_{i+1}-A_i, \ \ b_j = B_{j+1}-B_j$，我们也可以用一个形象的例子来解释 $A_i, B_j$ :

- 假设袋子 A 中有 $N$ 个物品，其价值为 $a_0, a_1, \dots, a_{N-1}$ 。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/tu-shu-lie-de-%20min-plus%20-juan-ji.html</guid><pubDate>Wed, 03 Jun 2026 08:21:12 +0000</pubDate></item><item><title>跟 Nachia DP 博客学DP-1 : 部分和DP</title><link>https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-1%20-%20-bu-fen-he-DP.html</link><description>### 跟 Nachia DP 博客学DP-1 : 部分和DP

Nachia 原文： [[DP 的俗称 | Mathenachia --- DP の俗称 | Mathenachia](https://www.mathenachia.blog/dp/)](https://www.mathenachia.blog/dp/)

&gt; 给定序列 $A = (a_1, a_2, \dots, a_N)$ 与非负整数 $S$.
&gt;
&gt; 询问是否存在一个下标集合 $Set$，使得 $\sum_{i \in Set} a_i = S$

这个问题是**部分和DP**的标准形式！状态可以这样设计：

&gt; [!tip]
&gt;
&gt; $ dp[n][s] = $ （当 $A = (a_1, a_2, \dots, a_N)$，$S = s$ 的时候的部分和问题的解.
&gt;
&gt; 其中 ($0 \leq n \leq N, 0 \leq s \leq S$)

通常这个二维数组的元素是布尔值，所以可以再添加维度，来扩展其额外信息！

转移过程往往是考虑从第 $i - 1$ 个数字的所有可达部分和转移到第$i$个数字的所有可达部分和的，这个部分并不难思考！


#### 例题：

这里先给出两个例题链接！想先思考一下的可以先不看下面的题解！

1 . [[ATC abc286D - Money in Hand](https://atcoder.jp/contests/abc286/tasks/abc286_d)](https://atcoder.jp/contests/abc286/tasks/abc286_d)  **EASY** 板子题

2.[[ATC abc275F - Erase Subarrays](https://atcoder.jp/contests/abc275/tasks/abc275_f)](https://atcoder.jp/contests/abc275/tasks/abc275_f)  **MID+**

------

------

### 题目讲解

1 . [[ATC abc286D - Money in Hand](https://atcoder.jp/contests/abc286/tasks/abc286_d)](https://atcoder.jp/contests/abc286/tasks/abc286_d)

 其实就是部分和 DP 模板

考虑如何转移，对于第 $i$ 个数字，有两种情况，首先 $a_i$ 本身可以达到，所以 $dp[i][a_i] = true$，然后考虑从 $i - 1$ 个数字转移，对于每一个在 $i - 1$ 层是 $true$ 的点，都可以有转移 $dp[i][* + a_i] = true,\ \  \because dp[i - 1][*] = true$.

这样这道题的复杂度就是 $O(\sum B \cdot X)$

```cpp
void Mainsol() {
	INT(N, X);
	v&lt;I&gt; v;
	rep(i, N) {
		INT(A, B);
		rep(B) v.pb(A);
	}
	sor(v); // 排不排序无任何区别
	vv(bool, f, sz(v) + 1, X + 1 ,false);
	f[0][0] = 1;

	rep(i, 0, sz(v)) rep(j, 0, X + 1) {
		if(j &lt; v[i]) {
			if(f[i][j]) f[i + 1][j] = 1;
			else f[i + 1][j] = 0;
		}
		if(j &gt;= v[i]) {
			if(f[i][j] || f[i][j - v[i]]) f[i + 1][j] = 1;
			else f[i + 1][j] = 0;
		}
	}
    
	if(f[sz(v)][X]) Yes();
	else No();
}

```

2.[[ATC abc275F - Erase Subarrays](https://atcoder.jp/contests/abc275/tasks/abc275_f)](https://atcoder.jp/contests/abc275/tasks/abc275_f)

考虑在部分和DP上加新的维度！

对于正常的部分和我们会发现，考虑第 $i$ 个元素的时候，有且仅有两种情况，选择第 $i$ 个元素与不选择第 $i$ 个元素.

题目要我们求删除的最小连续段数，所以如果我们把 **选择** 和 **不选择** 该元素作为维度加在部分和DP上，是不是就可以了呢？

现在让我们再考虑一下如何存储结果，对于如果选择这个数，那么最后一维是 $1$ ,反之是 $0$，然后考虑结果转移，如果前一个是 $0$，那么如果现在是 $1$ ，那么就需要贡献 $+1$ ，反之则不 $+1$ ，如果前一个是 $1$ 呢？其实都不添加贡献就可以了！

但是这里还要考虑初始值，因为还要确定能不能凑出 $S$，所以我们可以考虑把初始值都设置为 $\inf$，初始值$dp[0][0][0] = 0$ 即可！

这样复杂度也是 $O(N \cdot M)$ 的

```cpp
void Mainsol() {
	INT(N, M);
	VEC(I, a, N);
	vv(ll, f1, N + 1, M + 1, LINF);
	vv(ll, f0, N + 1, M + 1, LINF);
	f0[0][0] = 0;
	rep(i, N) {
		rep(j, M + 1) {
			chmin(f1[i + 1][j], f1[i][j]);
			chmin(f1[i + 1][j], f0[i][j] + 1);
			if(j + a[i] &lt;= M) {
				chmin(f0[i + 1][j + a[i]], f0[i][j]);
				chmin(f0[i + 1][j + a[i]], f1[i][j]);
			}
		}
	}
	dbg(f1[N]); dbg(f0[N]);
	rep(i, 1, M + 1) {
		ll ans = min(f1[N][i], f0[N][i]);
		if(ans == LINF) ans = -1;
		out(ans);
	}
}
```。</description><guid isPermaLink="true">https://SistineFibel-2006.github.io/post/gen-%20Nachia%20DP%20-bo-ke-xue-DP-1%20-%20-bu-fen-he-DP.html</guid><pubDate>Mon, 01 Jun 2026 14:46:26 +0000</pubDate></item></channel></rss>