暑假

P2569 https://www.luogu.com.cn/problem/P2569

参考这篇。

/*单调队列优化dp

买入股票的转移方程j是顺序枚举的,因为是买入股票,手中的股票应该是越来越多的,
当前的决策有可能在后面(j更大)的时候用到,所以你需要先求出来,
同理,卖出股票时,你手中的股票是越来越少的,
即当前的决策有可能需要用到比你手中股票数更少的状态,
所以需要提前处理,因此需要倒序枚举*/

https://www.luogu.com.cn/record/223728748

AT_arc098_b \(x \oplus y \oplus \cdots \oplus z \le x+y+..+z\)。异或是不进位的加法,所以有一部分不等全部也不会相等。

P2882 怎么 \(O(1)\) 处理 \(01\) 数列的区间翻转和实时查询?下面 \(a_i \oplus x\) 即为实际值,\(b\) 为差分数组,\(k\) 为翻转长度,那个 if 只是条件判断。

bool x=0;
for(int i=1;i<=n;i++)b[i]=0;
for(int i=1;i<=n;i++){
	x^=b[i];
	if(a[i]^x){
		x^=1;
		b[i+k]^=1;
	}
}

以下三个都相等。

  • 原图中两个点之间的所有简单路径上最大边权的最小值。
  • 最小生成树上两个点之间的简单路径上的最大值。
  • Kruskal 重构树上两点之间的 LCA 的权值。

P6835 https://www.luogu.com.cn/article/icz18zjz 建议再次手推式子。

最长公共子序列转移方程。

\[f_{i,j}=\max(f_{i-1,j},f_{i,j-1}) \]

\[if(a_i==b_j)\quad f_{i,j}=\max(f_{i,j},f_{i-1,j-1}+1) \]

P2516 https://www.luogu.com.cn/record/224495353

有三个难点。首先由题可知。

  1. \(a_i\)\(b_j\) 都属于所有的 \(lcs(i,j)\)
  2. \(a_i\) 属于所有的 \(lcs(i,j)\)\(b_j\) 并非属于所有的 \(lcs(i,j)\)
  3. \(a_i\) 并非属于所有的 \(lcs(i,j)\)\(b_j\) 属于所有的 \(lcs(i,j)\)
  4. \(a_i\)\(b_j\) 都并非属于所有的 \(lcs(i,j)\)

  1. \(a_i=b_j\),则\(f_{i,j}=f_{i-1,j-1}+1\),对应情况 \(1\)
  2. \(f_{i,j}=f_{i,j-1}\),则\(b_j\)并非属于所有的 \(lcs(i,j)\),对应的情况包括 \(2,4\)
  3. \(f_{i,j}=f_{i-1,j}\),则\(a_i\)并非属于所有的 \(lcs(i,j)\),对应的情况包括 \(3,4\)
  4. \(f_{i,j}=f_{i-1,j-1}\),则对应情况 \(4\),并且此时也必满足其上两种的条件。

  • 为什么减掉那块?根据容斥原理算重一次,所以减掉。
  • 滚动优化(容易写错)。因为转移最小从前一个开始所以第一维大小 \(2\) 就够了。拿新变量滚动第一维。
  • 初始化。第一维滚动了就不用初始化它了。其它边界记得设置最少一种方案不然没法累加。

Skip1

P2051 https://www.luogu.com.cn/problem/P2051
这种格子类的通通考虑状压。每行列最多两个炮。然后分讨这行用了 \(0,1,2\) 个炮即可。一道大的分类讨论而已。但是状态设计也是难点。

\(f_{i,j,k}\) 表示前 \(i\) 行有 \(j\) 列放了 \(1\) 个棋子,\(k\) 列放了 \(2\) 个棋子的方案数。

易错点是放 \(2\) 棋子。空白加一列的情况。因为这个时候少一列 \(1\) 又多一列 \(1\)。最后列数不变!

凸函数加一次函数还是凸函数。

P3605 子树差分 https://www.luogu.com.cn/problem/P3605

给定一棵 \(n\) 个点的树,每个点有点权,求每个点子树内点权大于自身的点的数量,\(n \le 10^5\)

大于某个数值的点的数量显然可以通过树状数组维护,关键是怎么精准地计算到单棵子树内。

如果在 DFS 过程中更新树状数组,这样维护的是 DFS 过程中到目前这个点为止各个数值出现的次数,不一定是单棵子树。

考虑 DFS 的过程中一个点会进出(递归与回溯)各一次,实际上进入这个点时,该点的子树信息还没更新到树状数组中,而出这个点时,该点的子树信息已经都加入到树状数组中,因此这两个时间点查询的差值就是该子树的贡献。这种思想被称为子树差分。

子树差分例题:https://www.luogu.com.cn/problem/P1600

popcount(a^b)=popcount(a|b)-popcount(a&b)=
popcount(a)+popcount(b)-2popcount(a&b)

https://www.luogu.com.cn/problem/P3051
https://www.luogu.com.cn/article/sb4oa22b

很应该知道的 trick。

拆环为链拆环为链!

经典题:一条链,给出若干线段,问:若要使线段完整覆盖这条链,最少选几条线段?

将所有候选线段按左端点 \(l_i\) 从小到大排序。如果左端点相同,则按右端点 \(r_i\) 从大到小排序。

假如当前选择的区间已经覆盖了前 \(x\) 个点,为了覆盖第 \(x+1\) 个点,一定要再选择一个左端点小于等于 \(x+1\) 的区间。那么我们加入一个左端点小于等于 \(x+1\),且右端点位置最大的区间,这样能尽可能覆盖最多的点,一定是最优的。

此时把 \(x\) 更新为这个右端点的位置。初始时令 \(x=0\),不断重复这个过程选择加入的区间,直到 \(x=n\) 时终止循环。若某一次选择区间后 \(x\) 仍不变,说明第 \(x+1\) 个点不可能被覆盖,无解。

对每个 \(1 \le i \le n\) 预处理出左端点小于等于 \(i\) 的所有区间中最大的右端点位置,就可以用 \(O(n+k)\) 的时间预处理,\(O(n)\) 选择区间,总时间复杂度 \(O(n+k)\) 解决这个问题。

环的话 P6902 就拆,但是还要加一个倍增优化成 \(O(n \log n\))。https://www.luogu.com.cn/problem/P6902

https://www.luogu.com.cn/record/225195227

数组越界风险‌:在 for(int j=0;j<=vec[i].size()-1;j++)循环中,如果vec[i]为空,vec[i].size()-1会变成无符号整型的最大值(因为size()返回无符号数),导致循环条件异常,建议改为for(int j=0;j<=(int)vec[i].size()-1;j++)或使用迭代器。

Skip2

喵喵题 P2519 https://www.luogu.com.cn/problem/P2519
https://www.luogu.com.cn/article/jczkipeh

但是重合区间数如果超过区间长度就一定有假话。

这个很好理解,假如区间长度为 \(k\),那么就是说有k个人分数相等,但却有多于 \(k\) 个人说自己在这段区间内,这显然有人说假话。

蓝题好多喵喵题,喵喵题都是豪题。

https://www.luogu.com.cn/problem/AT_dp_t
https://www.luogu.com.cn/problem/P2467
https://www.luogu.com.cn/article/o0oht8bj
https://www.luogu.com.cn/article/sg1fwhh5

转移方程都是类似的,用来处理波形。
\(f_{i,j}\) 表示已经考虑了前 \(i\) 个位置,且第 \(i\) 位上,填的数字是第 \(j\) 小的总方案数。(由于是排列所以第 \(j\) 小等价于这位填的是 \(j\))。

P1070 我终于会正解了!!!

https://www.luogu.com.cn/article/5p3x7ruk

神仙处理:先是要想到固定机器人转工厂和金币。其次是聪明的转移变形。

P6647 https://www.luogu.com.cn/record/225529688

P3188 很有意思的 dp。

连通图最长路求法:拓扑排序加 dp。

  1. 拓扑排序确定节点处理顺序。
  2. 定义 \(dp_i\) 表示以节点 \(i\) 结尾的最长路径长度。
  3. 递推:\(dp_i=max(dp_j+w_{j,i})\),对所有入边 \(j\rightarrow i\)

Skip 3

P1600 https://www.luogu.com.cn/problem/P1600

题解:https://www.luogu.com.cn/article/nkwnx1ti

分讨两段路 \(s \rightarrow lca\)\(lca \rightarrow t\),推一下能被观察到的条件,用子树差分做即可。

https://www.luogu.com.cn/record/226182410

子树结点的 DFS 序构成一段连续区间,可以直接遍历。

圆方树的点数小于 \(2n\),这是因为割点的数量小于 \(n\),所以请注意各种数组大小要开两倍。

欧拉路径伪代码。

void dfs(int now)
{
	for(int i=del[now];i<G[i].size();i=del[now])
	{
		del[now]=i+1;
		dfs(G[now][i])
	}
	st.push(now);
 }
 //其中 del[now] 表示 G[now][1,2……,del[now]-1] 
//都已被标记,下一次要从G[now][del[now]]开始访问。

‌SPFA 算法允许一条边被多次经过,Dijkstra算法不允许一条边被多次经过。

CF609E:\(n\) 个点,\(m\) 条边,如果对于一个最小生成树中要求必须包括第 \(i(1 \le i \le m)\) 条边,那么最小生成树的权值总和最小是多少。\(1 \le n \le 2 \times 10^5\)\(n-1 \le m\le 2 \times 10^5\)

求出最小生成树,添加第 \(i\) 条边,之后出现一个环。把环上权值最大的边断掉就可以了。当然不能断自己,所以断的是原 mst 上 \(u\rightarrow v\) 权值最大的边。做个树剖维护一下就行了。

同余最短路。

https://www.luogu.com.cn/problem/P3403

https://www.luogu.com.cn/problem/P2371

多点最短路的最小值化两点最短路。

https://www.luogu.com.cn/problem/P5304

分为两个集合,确保集合之间的点一定不构成最小的最短路,之后用起点 \(s\) 连点集 \(1\)(边权为 \(0\)),终点 \(t\) 连点集 \(2\),点集 \(1,2\) 相连。那么原问题变成了 \(s\rightarrow t\) 的最短路。

重点是怎么划分点集。https://www.luogu.com.cn/problem/solution/P5304?page=1

https://www.luogu.com.cn/problem/P3199

\(ans\) 为带权有向图所有环的平均值的最小值。对于任意有向图 \(G\),添加一点 \(s\) 后。

\[ans=\min_{v \in V,F_n(v)\ne \infty }\max_{0 \le k \le n-1}[\frac{F_n(v)-F_k(v)}{n-k} ] \]

其中 \(F_k(v)\)\(s \rightarrow v\) 恰好走了 \(k\) 条边的最短路(不存在则为 \(\infty\))。

\(01\) 分数规划:https://blog.csdn.net/niiick/article/details/80925267

Skip 4

https://www.luogu.com.cn/problem/P3430

给定两个数组,要求每个数组中不可以出现相同的数字。两个数组同一位置的数可以互换,最少换几次满足上述要求?


如果两个相同的数在不在同一行,给两个数所在的列的编号连一条边权为 \(0\) 的边,否则连一条边权为 \(1\)的边。

给每个联通块的点黑白染色(\(1\) 边连接的点颜色相反,\(0\) 边连接的点颜色相同),答案每次加上每个联通块黑点和白点个数的最小值。

https://www.luogu.com.cn/article/rfudqj2s

题解有一点不清楚,这里感谢群内的 VainSylphid 大佬解答。以下是原话:

你发现 \(1\) 边相当于,我们希望这两列中恰有一列做交换。\(0\) 边相当于,如果这两列中一列交换了,另一列也要交换。

那你按照那个题解的方式染色,就相当于同一个颜色的必须全部交换或者全部不交换。

并且黑色和白色只有一种颜色全部交换,另一种全部不交换。

那取较小值就是显然的了。

2025.7.28-7.29 斜率优化专题

今天是斜率优化专题。https://www.bilibili.com/video/BV1dKSJY5Ew5

P2365 https://www.luogu.com.cn/article/367gcax3

费用提前计算,牛的。只需要把 \(s\)\(j+1\) 后的影响补充到费用中就可以了。新段的费用已经叠进 \(f\) 里了。

https://www.luogu.com.cn/record/227393110

移项要遵循的原则是:

  • 把含有 \(function(i)\times function(j)\) 的表达式看作 \(kx\)
  • 含有 \(dp_i\) 的项必须要在 \(b\) 的表达式中。
  • 含有 \(function(j)\) 的项必须在 \(y\) 的表达式中。
  • 如果未知数 \(x\) 的表达式单调递减,最好让等式两边同乘个 \(−1\),使其变为单增。

精简记忆版本:

  • 交叉项 \(kx\)
  • \(dp_i \rightarrow b\)
  • \(fuc_j \rightarrow y\)

用单调队列维护凸包点集,操作分三步走:

  1. 进行择优筛选时,在凸包上找到最优决策点 \(j\)
  2. 用最优决策点 \(j\) 更新 \(dp_i\)
  3. \(i\) 作为一个决策点加入图形并更新凸包(如果点 \(i\) 也是 \(dp_i\) 的决策点之一,则需要将 3 换到最前面)。

Skip 5

https://www.luogu.com.cn/problem/P5677

好题喵。

:::info
排序,处理所有好对。离线,左端点从小到大排序,有小于等于当前枚举 \(r\) 的线就不断加进来,再去掉 \(l\) 之前的线就是当前贡献。
:::

https://www.luogu.com.cn/record/228032569

P7706 https://www.luogu.com.cn/problem/P7706

不会啊,思考 10 分钟无果,遂怒。厉害的题目,其实很板,只是我不会这个 trick。https://www.luogu.com.cn/article/oj2vo7sx 。讲的很清楚无需赘述了。

https://www.luogu.com.cn/problem/P3792
https://www.luogu.com.cn/problem/P5278

“双倍经验”,https://www.luogu.com.cn/article/itudyc23 学习思路。

在洛谷 P5278 题中,维护前驱的最大值(即区间内每个元素上一次出现位置的最大值)的核心目的是快速检测元素重复性。若该最大值大于等于区间左端点,说明至少有一个元素在区间内重复出现。

置换环 https://www.cnblogs.com/TTS-TTS/p/17047104.html

  • 欧拉序 LCA 算法

‌预处理复杂度‌:\(O(n + n \log n) = O(n\log n)\)

查询复杂度‌:\(O(1)\)

‌实现方式‌:将 LCA 问题转化为 RMQ 问题,使用 ST 表解决。

  • 倍增 LCA 算法

‌预处理复杂度‌:\(O(n\log n)\)

‌查询复杂度‌:$O(\log n) $。

‌实现方式‌:预处理每个节点的 \(2^k\) 级祖先,使用倍增思想进行查询。

虚树中,可以发现每个蓝点对应一个连通块,且连通块大小为该蓝点对应原树中子树大小减去蓝点儿子子树大小。

Skip 6

假设 \(G=(X,Y,E)\) 是二分图,且 \(\left | X \right | \le \left | Y \right |\)。对于任意 \(W\subseteq X\),记 \(V(W)\)\(G\) 中所有与 \(W\) 的顶点相邻的顶点集合。那么当且仅当 \(\left | W \right | \le \left | V(W) \right |\) 对于所有 \(W\subseteq X\) 都成立, 存在对于图 \(G\) 的一个匹配 \(M\) 使得 \(X\) 中的所有点都是匹配点

所有正则的二分图都有完美匹配。(正则二分图中,所有顶点的度数都相同)。

删除操作可以离线倒过来变成加入操作。


有一个 \(n\times m\) 的矩阵 \(A\)\(A\) 中每个元素都是 \([0,10^5]\) 中的整数。且形式化地,有

\[E_{i,j}=\sum_{1\le x\le n}\sum_{1\le y\le m}(|x-i|+|y-j|)A_{x,y} \]

现在要用矩阵 \(E\) 还原出一个符合上述条件的整数矩阵 \(A\)。不过还原出的 \(A\) 中的每个元素均属于 \([0,10^{16}]\) 即可。

要求单次时间复杂度为 \(O(nm)\)


怎么这么牛的一道题。

设:

\[C_i=\sum_{j=1}^{m}A_{i,j} \]

\[E_{i,j}=\sum_{1\le x\le n}\sum_{1\le y\le m}(|x-i|+|y-j|)A_{x,y} \]

由这个式子,可以注意到:

\[E_{i,j}=\sum_{x=1}^{n}\left |x-i \right |\times C_x+\sum_{x=1}^{m}\left |x-j \right |\times R_x \]

进一步的有:

\[E_{i-1,j}+E_{i+1,j}-2\times E_{i,j}=2\times C_i \]

其中 \(2 \le i \le n-1\)

同理可以解出 \(R_j\)

\[R_j=\sum_{i=1}^{n}A_{i,j} \]

也就是说,确定不了的只剩下 \(C_1,C_n,R_1,R_m\)

考虑贪心,我们依次考虑每个 \(A_{i,j}\),直接将其为 \(\min(R_i,C_i)\),然后把 \(R_i\)\(C_i\) 同时减去 \(A_{i,j}\),一直接着做下去。那么只要有解,这样做就一定能构造出一个 \(A_{i,j}\) 为非负整数的解。

于是这道神仙题就做完了(吗?)。

:::info
注意这里需要特判 \(n=1\)\(m=1\) 的情况!这两个需要特判换一个解法!
:::

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m;
void solve(){
	cin>>n>>m;
	vector<vector<int> >E(n+1),A(n+1);
	vector<int>C(n+1),R(m+1);
	For(i,1,n){
		E[i].resize(m+1);
		A[i].resize(m+1);
	}
	For(i,1,n)For(j,1,m)cin>>E[i][j];
	For(i,2,n-1)C[i]=(E[i-1][1]+E[i+1][1]-2*E[i][1])/2;
	For(i,2,m-1)R[i]=(E[1][i-1]+E[1][i+1]-2*E[1][i])/2;
	if(n==1&&m==1){cout<<E[1][1]<<"\n";return ;}
	if(n==1){
		R[1]=E[1][m];
		For(i,2,m-1)R[1]-=(m-i)*R[i];
		R[m]=E[1][1];
		For(i,2,m-1)R[m]-=(i-1)*R[i];
		R[1]/=m-1,R[m]/=m-1;
		For(i,1,m)cout<<R[i]<<" ";
		cout<<"\n";
		return;
	}
	if(m==1){
		C[1]=E[n][1];
		For(i,2,n-1)C[1]-=(n-i)*C[i];
		C[n]=E[1][1];
		For(i,2,n-1)C[n]-=(i-1)*C[i];
		C[1]/=n-1,C[n]/=n-1;
		For(i,1,n)cout<<C[i]<<"\n";
		return;
	}
	int X=E[1][1];
	int Y=E[1][m];
	int Z=E[n][1];
	For(i,2,n-1){
		X-=(i-1)*C[i];
		Y-=(i-1)*C[i];
		Z-=(n-i)*C[i];
	}
	For(i,2,m-1){
		X-=(i-1)*R[i];
		Y-=(m-i)*R[i];
		Z-=(i-1)*R[i];
	}
	int W=0;
	For(i,2,m-1)W+=R[i];
	For(i,2,n-1)W-=C[i];
	W=W*(n-1)*(m-1);
	W-=(m-1)*Z-(n+m-2)*X-(n-1)*Y;
	W/=n-1,W/=2*n+2*m-4;
	C[n]=W;
	C[1]=(Z-X)/(n-1)+C[n];
	R[1]=(Y-(n-1)*C[n])/(m-1);
	R[m]=(X-(n-1)*C[n])/(m-1);
	For(i,1,n){
		For(j,1,m){
			A[i][j]=min(C[i],R[j]);
			C[i]-=A[i][j];
			R[j]-=A[i][j];
		}
	}
	For(i,1,n){
		For(j,1,m){
			cout<<A[i][j]<<" ";
		}
		cout<<"\n";	
	}
} 
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int T;cin>>T;
	while(T--)solve();
	return 0;
}

Skip 7

两个栈,每个栈中均有 \(n\times K\) 个元素,其中数字 \(1,2,\cdots, n\) 各分别出现了恰好 \(K\) 次。

不断进行如下操作,直到有一个栈变为空:

  • 如果当前两个栈的栈顶元素相同,则将两个栈的栈顶元素都弹出,获得 \(1\) 的得分。
  • 如果当前两个栈的栈顶元素不同,则选择一个栈的栈顶元素弹出,不得分。

计算通过合理的操作,最大得分是多少。

对所有数据,保证 \(1\le n\le 10^4,1\le K\le 16\)


考虑所有得分的时刻,把配对上的两个元素在原栈中的位置标出来,那么得到两个单调递增序列 \(x,y\)。其中满足 \(a_{x_i}=b_{y_i}\)

反之,如果两个序列满足上面的要求,那么就可以通过⼀定的操作得到分数。于是目标就变成了在满足条件下最大化序列⻓度。

把所有可能的 \((x_i,y_i)\) 拿出来,定义全序关系 \(i < j\) 时有 \((x_i,y_i)<(x_j,y_j)\)。(这里的小于是两维度都小于)。共 \(nK^2\) 对。

:::info
为什么是 \(nK^2\) 对?因为共 \(n\) 个数字,每个数字在第一个栈里出现 \(K\) 次,也在第二个里出现 \(K\) 次。
:::

找最长上升子序列即可。别被诈骗了。

#include<bits/stdc++.h>
using namespace std;
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=2e5+10;
int n,k,a[N],ans;
int t[N],p[N][20],cnt[N];
inline int qry(int x){
	int res=0;
	for(;x;x-=x&(-x))res=max(res,t[x]);
	return res;
}
inline void upd(int x,int v){
	for(;x<=n*k;x+=x&(-x))t[x]=max(t[x],v);
}
int main(){
	cin>>n>>k;
	For(i,1,n*k)cin>>a[i];
	For(i,1,n*k){int x;cin>>x;p[x][++cnt[x]]=i;}
	For(i,1,n*k){
		for(int j=k;j>=1;j--){
			int res=qry(p[a[i]][j]-1)+1;
			ans=max(ans,res);
			upd(p[a[i]][j],res);
		}
	}
	cout<<ans;
	return 0;
}

Skip 8

判断 \(k\) 个数中 \(1\)\(k\) 每个值正好各出现一次

\(1\)\(k\) 赋值成随机 \(64\) 位无符号整数 \(p_i\)
预处理 \(s_i\) 对于每个 \(i (1\le i\le k)\)\(1\)\(i\) 异或和。
判断 \(k\) 个数的权值异或和是否等于 \(s_k\)

\(\max(p,q)-\min(p,q)=\left |p-q \right |\)

\(mex=i \rightarrow\)\(mex \geq i\)

不重不漏找子集:for(int i=u;i;i=(i-1)&u)f[u]+=f[i];//i是u的子集

对于树上任何一个结点 \(u\),他到根的路径上重链数不会超过轻边数 \(-1\)

对于树上任何一个结点 \(u\),他到根的路径上的重链条数不会超过 \(O(\log n)\),轻边条数不会超过 \(O(\log n)\)

Skip 9

长为 \(n\) 的序列 \(a\) 满足\(0\le a_i < 2^m\) 且序列中的数字两两不同。约定 \(F(x)\)\(\forall a_i, a_i\rightarrow a_i \oplus x\) 后序列逆序对的个数。

现在需要找到第 \(k\) 小的 \(x\)。称 \(x_i<x_j\),当且仅当 \(F(x_i)<F(x_j)\),或者 \(F(x_i)=F(x_j)\)\(x_i<x_j\)

对于 \(100\%\) 的数据,\(1\le T\le 10\)\(1\le n,\sum n\le 10^6\)\(1\le m\le 30\)\(0\le a_i < 2^{m}\)\(1\le k\le 2^m\)。保证 \(a_i\) 互不相同。

异或后比大小可以拆位考虑一下。令两数最高不同的二进制位为 \(L\),则两数大小关系改变当且仅当 \(x\) 的第 \(L\) 位为 \(1\)

:::info
这是因为前 \(L-1\) 位相同,异或同一个数字没影响,因为第 \(L\) 位不管 \(x\) 数字是什么,两个数这一位必定不相同,所以后 \(n-L\) 位的值对大小没影响。

那么易得出 \(x\) 这一位为 \(1\) 可以改变大小关系。

\(0 \oplus 1=1,1\oplus 1=0;0 \oplus 0=0,1\oplus 0=1\)
:::

因此每个二进制位是独立的。考虑求出初始逆序对个数,以及每个二进制位异或上 \(1\) 时逆序对的改变量。
前者可以通过归并排序或树状数组轻松求出。

:::info
逆序对的改变量指的是:把前面二进制位相同的放一组,每组求逆序对。
:::

对于后者,从高到低枚举二进制位,对这一位不同的数求逆序对(只有 \(0/1\)),然后根据这一位将序列分为两个子序列,对两个序列分别往下做。

那么这部分时间复杂度为 \(O(nm)\)

那么接下来问题就变为:有 \(m\) 个数,从中选出若干个数并求和,要求找到第 \(k\) 小的和。先把 \(m\) 个数分成两份,每份 \(\frac{m}{2}\) 个数,然后对每份求出所有选法的和,并排序。时间复杂度 \(O(m2^{\frac{m}{2}})\)

这样就变成从两个序列中各选一个进行组合。然后二分答案 \(s\),二分时计算出有多少种选法的和小于等于 \(s\)。这里可以用双指针在两个序列上扫。这部分时间复杂度为 \(O(2^{\frac{m}{2}}\log n)\)

Skip 10

遇到删除就时光倒流做添加。

“两点 \(u,v\) 存在两条边不相交路径”与“\(u,v\) 属于同一个边双”是互为充要条件的。

:::info
Menger 定理:在一个无向图中,两个不同顶点 \(u\)\(v\) 之间的最大/边不相交/路径数,等于将 \(u\)\(v\) 断开所需删除的最少边数。
:::

题目

简化版题目为 LeetCode 253,以下是简化版本的解法。

每支乐队的演唱会时间不能相交,等价于“为每个区间分配一个颜色,且相交的区间颜色不同”。我们需要最少的颜色数,这就是区间图的最小着色数。

对于任意区间集合,最小着色数 \(=\) 区间集合在任意时刻的最大重叠次数

步骤如下。备注:不管区间有无无包含关系,这样的算法都是正确的。因为考虑堆内的元素如果现在能用之后也能用,现在用掉一定不劣。而且不会因为排序对后面产生不同影响,因为因为左端点相同的都放一块了,扫到下一个的时候早就已经全部入队了。

  • 首先,对区间按左端点升序排序。

  • 然后,用小根堆优先队列,计算最大重叠次数。

    • 对于当前区间 \([l_i, r_i]\),先检查堆顶元素(最早结束的演唱会结束时间 \(top_r\))。
    • \(top_r < l_i\),即最早结束的演唱会在当前演唱会开始前已结束,无重叠。则弹出堆顶元素。该乐队可承接当前演唱会,无需新增乐队。
    • 将当前区间的右端点 \(r_i\) 加入堆,表示新增一场正在进行的演唱会。若堆大小增加,则可能需要更多乐队。
    • 堆的大小即当前同时进行的演唱会数量,也就是此时需要的乐队数,取最大值即为答案。

说回本题。

考虑确定时间段区间怎么求乐队数量。从左往右依次贪是对的,现在能继承⼀定是继承更优,记录⼀下现在还在堆里的右端点,如果最左边的那个 \(\le l\),弹出并把 \(r\) 加⼊,代价为 \(0\)。否则直接把 \(r\) 加⼊,代价为 \(1\)

考虑每个时间段的覆盖次数 \(t_i\),根据上述分析答案就是 \(\max t_i\)

直接 dp \(t_i\),令 \(t_0=t_{n+1}=0\)。注意到序列 \(t\) 合法当且仅当 \(\left | t_{i+1}-t_i \right |\le 1\)

还需要考虑 \(t_i\) 对应多少个区间集合。\(t_i=t_{i+1}\) 考虑被 \(i\) 覆盖的每个区间,要么不变;要么左端点最左边的那个在 \(i\) 结束,同时在 \(i+1\) 新开一个区间。一共 \(2\) 种方案。

\(k=\sum_i[t_i \ne 0 \ \land \ t_i=t_{i+1}]\),那么贡献就是 \(2^k\)。这⾥的⽅案对其他位置的影响是独⽴的,因为只需要考虑相邻位置的映射,所以贡献是 \(2\)。其余情况映射都是唯⼀的。

因为合法的区间集合与满足 \(|t_{i+1}-t_i| \leq 1\) 的覆盖次数序列 \(t\) 是一一对应关系,且序列中相邻位置 \(t_i\)\(t_{i+1}\) 的关系直接决定了区间在 \(i\)\(i+1\) 处的增减(对应区间开始或结束),仅需通过相邻位置的 \(t\) 就能唯一确定区间集合的结构,进而计算其贡献,无需考虑非相邻位置。

暴力 dp 即可,是 \(O(n^3)\) 的。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
const int p=998244353;
int n;
/*动态规划计算当最多使用
k支乐队时的合法集合数量
冲突的定义:
两个演唱会的时间区间存在重叠
包括端点处重叠*/ 
ll dp(int k){
    /*f[i]表示考虑完所有时间段后,
	被选中的演唱会中最大冲突深度为i的合法集合数量
	f[i]统计范围是所有选中的演唱会彼此完全不相交的集合
	例如 [1,2]、[3,4]、[5,6] 这样的时间段,
	任意两个都不重叠,同时进行的演唱会最多只有 1 个*/
    vector<ll>f(n+1),g(n+1);
    f[0]=1;
    For(i,0,n-1){
        fill(g.begin(),g.end(),0);
        For(j,0,k){
            /*情况1:不选当前时间段 
			或选了但不增加冲突深度
			(j=0时无冲突*/
            g[j]=(g[j]+f[j]*(j?2:1))%p;
            //情况2:从深度j+1转移到j
            if(j+1<=k){
                g[j]=(g[j]+f[j+1])%p;
            }
            //情况3:从深度j-1转移到j
            if(j-1>=0){
                g[j]=(g[j]+f[j-1])%p;
            }
        }
        swap(f,g);
		//新状态变成旧状态,旧状态直接等覆盖 
    }
    //返回最多使用k支乐队的合法集合总数
    return (f[0]+f[1])%p;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n;
    For(k,1,n)cout<<dp(k)<<" ";
    return 0;
}

tarjan 算法告诉我们:如果 \(y\)\(x\) 的子节点且 \(low(y)≥dfn(x)\),那么 \(x\) 就是割点。若根 \(x\) 有两颗及以上的子树,那么 \(x\) 即为割点。

差分约束求最长路,得到的解在有下界约束的情况下,字典序最小。

若一条路径的 lca 在另一条路径上,那么这两条路径有交,否则无交。

一个点集的 lca 为这个点集中 dfn 最大点和 dfn 最小点的 \(lca\)

两点间路径最大值的最小值是 kruskal 重构树上两点 LCA 的点权。

\(\operatorname {mex}\) 等于补集 \(\operatorname {min}\)

对于每个不是最短路上的点 \(u\)\(1\)\(u\) 的最短路必定会使用最短路的一段前缀。

两点间存在长为奇数的简单路径,当且仅当两点生成树上的路径存在一条边属于某个简单奇数环。

一条边属于某个简单奇环,当且仅当其所在的点双连通分量不是二分图。

posted @ 2025-10-14 21:09  Accept_Reality  阅读(12)  评论(0)    收藏  举报