基础常见 DP 优化以及图上问题、思维题、数据结构题、计数题、字符串 trick

计数题 trick

本质上对于一个计数方案你需要找到一个唯一的描述(即状态与方案形成一对多的关系,且一个方案仅能被一个状态描述)。当然也有特殊情况,比如你实在想不到怎么让一个方案仅被一个状态描述,且方案包含选若干个本质相同的元素这样类似的限制,也可以容斥(二项式反演)一下。其他情况下如一些经典的排列计数问题,不难发现这些问题中只有排列的顺序是重要的,这时候就可以推导出一些 DP 转移。经典应用是模拟赛记录 9/11的 T1 和AT_agc024_e [AGC024E] Sequence Growing Hard

AT_agc024_e 的简要题解

考虑字典序比较过程,容易得到从 \(A_n\)\(A_1\) 所做的事情无非就是删掉末尾一堆连续相同的数的其中一个(删哪个都一样,不妨直接视作删最末尾的数),或者找一个 \(a_i>a_{i+1}\) 的位置然后把 \(a_i\) 给删掉。考虑到最小数的特殊性,我们不妨枚举序列中第一个 \(1\) 的位置 \(k\) 和其被删除的时间 \(p\),前提是这个序列中有 \(1\),然后令 \(f_{i,j}\) 表示 \((A_0,A_1,\dots,A_N)\) 数列组中每个数都由 \([1,j]\) 构成的方案数。

如果序列中没有 \(1\),让 \([1,j-1]\) 双射对应到 \([2,j]\),那么 \(f_{i,j}\leftarrow f_{i,j-1}\)

否则有转移:

\[f_{i,j}\leftarrow \sum_{k=1}^i\sum_{p=i-k+1}^i\begin{pmatrix}p-1\\i-k\end{pmatrix}f_{i-1,j-1}\times f_{i-k,j} \]

注意到中间那个组合数和 \(j\) 无关,预处理即可 \(O(n^3)\) 解决。

这个转移是一种顺序双射的经典方式。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=305;
int n,lim;
LL MOD,f[N][N],C[N][N],preC[N][N];
void init(){
	for(int i=0;i<=n;i++)
		C[i][0]=C[i][i]=1;
	for(int i=2;i<=n;i++)
		for(int j=1;j<i;j++)
			C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>lim>>MOD;
	init();
	for(int i=1;i<=n;i++)
		for(int k=1;k<=i;k++)
			for(int p=i-k+1;p<=i;p++)
				(preC[i][k]+=C[p-1][i-k])%=MOD;
	for(int i=0;i<=lim;i++)
		f[0][i]=1;
	for(int j=1;j<=lim;j++)
		for(int i=1;i<=n;i++){
			for(int k=1;k<=i;k++)
				(f[i][j]+=f[k-1][j-1]*f[i-k][j]%MOD*preC[i][k]%MOD)%=MOD;
			(f[i][j]+=f[i][j-1])%=MOD;
		}
	cout<<f[n][lim];
	return 0;
}

DP 状态定义提示

  • 不妨尝试套路化的定义,如,做前 \(i\) 个的答案 \(f_i\)

  • 部分分裂性问题可以转为树上合并性问题,因为相同信息子结构具有重复性,且分裂后大量信息没有应用。

  • 利用正难则反的 trick,套用思维题一定转化后定义。

  • 不妨考虑局部对整体的贡献系数,如(简记)一类博弈论 DPCF1810G 题解题解:P6811 「MCOI-02」Build Battle 建筑大师

  • 在此基础上,可以把 DP 转移转化成势能累加/类乘,若干次操作兴许可以通过组合意义优化为走若干步到某个状态的问题,同样需要统计贡献系数(如P14461 【MX-S10-T2】『FeOI-4』青年晚报题解:P14461 【MX-S10-T2】『FeOI-4』青年晚报)。

  • 设 DP 式子尽量让下标与绝对值而非相对值有关,这样在不少 DP 优化中都有用(包括但不限于线段树优化 DP线段树合并决策单调性等)。

  • 可行性 DP 中,不妨把一个维度放在 DP 值上。

  • 贡献延后 trick,详见CSP-S 2025 T4

  • 转移中容斥,这种问题通常有两种选择,且其限制互为补集,对其中一个方向容斥即可,详见CSP-S 2025 T4

  • 不妨先从题目部分分、性质入手设计方程与转移,尝试弱化题面。

DP 优化

辅助数组

P5997 [PA 2014] Pakowanie

考虑在该题的时限下,朴素的 \(O(nm2^n)\) 的做法会爆掉,可以优化成 \(O(n2^n)\)。具体地,记录一个数组 \(g[i]\) 表示做到集合 \(i\) 时当前背包剩余容量。再加入一点贪心思想,先将背包的容量从大到小排序,先取完前面再取后面即可解决。

展示一个 \(n\) 位二进制数:

void print(int x){
	int u=0;
	for(int i=1;i<=n;i++)u=(u<<1)+(x&1),x>>=1;
	for(int i=1;i<=n;i++)cout<<(u&1),u>>=1;
}

状态优化

当状态必定设计为形如 \(dp[i][j][a_i]\) 的形式时,考虑到 \(a_i\) 一定取得到,即对于确定的 \(i\) 该项相当于一个常数,所以可以直接把这一维压缩掉。典型例题:P7685 [CEOI 2005] Mobile ServiceAT_arc073_d [ARC073F] Many Moves

进阶的比如题解:P12546 [UOI 2025] Convex Array也是进行了状态优化,压缩之后对于每个 \(i\) 只需要存储最优的 \(O(1)\)\([2][3]\) 状态和在 STL map<int,int> 上存 \(O(n)\)\([1]\),类似滚动数组的思想空间就可以达到 \(O(n)\),转移时间是 \(O(n\log n)\) 的。

数学优化

P7801 [COCI 2015/2016 #6] KRUMPIRKO

\[\begin{aligned} ans&=\frac{Y(\sum c_i -Y)}{X(\sum a_i -X)} \end{aligned} \]

其中 \(X,Y\) 是强关联的,不好直接计算。

考虑固定 \(X\) 到数组下标上,在数据范围允许的情况下我们处理出 \(f_{i,j,k},g_{i,j,k}\) 分别表示能取到的 \(Y\)\(\min,\max\),在考虑前 \(i\) 个,取了 \(j\) 个,\(X=k\) 的情况下,滚动数组优化即可。由于下面是定值的时候,上面就是一个二次函数,开口朝下,于是最小值一定在定义域的最左边或最右边。

\[f_{i,j,k}\leftarrow\max(f_{i-1,j,k},f_{i-1,j-1,k-a_i}+c_i) \]

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
int n,L,a[105],c[105],X,Y;
int f[105][505],g[105][505];
double ans=1e12;
int main(){
	scanf("%d%d",&n,&L);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),X+=a[i];
	for(int i=1;i<=n;i++)scanf("%d",&c[i]),Y+=c[i];
	for(int j=0;j<=L;j++)
		for(int k=0;k<=X;k++)
			f[j][k]=INF,g[j][k]=-INF;
	f[0][0]=g[0][0]=0;
	for(int i=1;i<=n;i++){
		for(int j=min(i,L);j>=1;j--){
			for(int k=X;k>=a[i];k--){
				f[j][k]=min(f[j][k],f[j-1][k-a[i]]+c[i]);
				g[j][k]=max(g[j][k],g[j-1][k-a[i]]+c[i]);
			}
		}
	}
	for(int s=1;s<X;s++){
		if(f[L][s]<Y)ans=min(ans,1.0*f[L][s]*(Y-f[L][s])/(1.0*s*(X-s)));
		if(g[L][s]>0)ans=min(ans,1.0*g[L][s]*(Y-g[L][s])/(1.0*s*(X-s)));
	}
	printf("%.3lf",ans);
	return 0;
}

图上(树上)问题 trick

  • 点边互化
    事实上,这个东西有两种转化方法:

    1. 边化点:
    • 对于每条边开一个辅助点(边化点),这是 LCT 常用维护边权技巧。
    • 在一棵有根树上,对于每条边,把它的边权扔到它连接的两个节点中深度较大的那一个上面作为点权(边化点),这是某些思维题的常用技巧(题解:AT_agc052_b [AGC052B] Tree Edges XOR)。
    1. 点化边,但是对于一颗菊花会出现一个点(转化后的一条边)需要连接多条边(转化后的多个点),所以这个东西怎么用我也不知道。

图结构判断 trick

  • 每个点作为入点和出点分别恰好出现 \(1\) 次,是若干个环。

  • 每个点恰有 \(1\) 出度,内向基环树森林。

  • 每个点恰有 \(1\) 入度,外向基环树森林。

思维题 trick

  • 时光倒流

  • 离线操作

  • 考虑不同对象对答案贡献

  • \(\min(a,b)\)(分类讨论,通常为一定值一变量,如题解:P10789 [NOI2024] 登山等)

  • 统计答案是否连续?(如一段前缀后缀

  • 区间交集

  • 区间并集(将所有区间按照左端点升序排序,记一个 \(maxr\) 贪心即可)

  • 区间最小点覆盖(将所有区间按右端点升序排序,记一个 \(maxl\) 贪心即可)

  • 折半搜索

  • 二进制分组

  • 拆绝对值:\(|a-b|=\max(a-b,b-a)\)

  • 矩形操作与 DP 问题考虑先固定矩形的一些边(如上界和左右界等)再计数/DP。

  • DP 转移注意是否具有决策单调性,可使用指针优化一维复杂度(AT_agc033_d [AGC033D] Complexity)。

  • 正难则反(考虑\(\text{全局}-\text{非法情况}\)

  • ...(后续补充)

数据结构题 trick

区间查询问题,比较难以直接回答时,可以考虑离线下来处理,把query放进其右端点的vector中。

(简记)扫描线 离线二维数点

数据结构(线段树)优化 DP

通常需要设出状态 \(f_i\) 的形式,然后下标需要是绝对而非相对的否则不好转移。诸如P9871 [NOIP2023] 天天爱打卡这类问题需要注意离散化的有效点是否影响答案计算。

有一类问题诸如P14255 列车(train)需要注意到 \(f_i\) 的单调性,包括区间覆盖时对答案的影响,\(f_v-f_r\) 作为最小值的计算。同时注意这种求一个最小权值合法区间包含已选区间的东西可以使用线段树维护的套路。

计数类问题优化

存在连续区间 \([l,r]\) 且答案要求最大化/最小化贡献与区间的 \(\min\max\) 或次大次小有关时,可以对于每个点单独考虑其可能作为最大值/最小值/次大值/次小值的区间,这样就固定下来了区间这四个值。前两个可以直接单调栈做,后两个需要搞一个双向链表,从小到大/从大到小排序,顺次遍历,每次删掉这个元素,其前驱的若干个数就分别是前面第 \(x\) 个大于/小于它的数,以此类推。

例题:01trie P4098 [HEOI2013] ALO

贪心 trick

环形问题一般优先考虑破换成链,然后就可以变成一般化 DP 问题。当然,有可能不能简单地这么处理。

看一类经典贪心:P1031 [NOIP 2002 提高组] 均分纸牌

这需要我们对每个 \(i\) 单独考虑向一边传递的数量(不妨令为左边),并且如果是获得而不是传递那么该值就是负数。我们不妨令这个值为 \(X_i\),表示 \(i\) 向左传递的数量。那么其实传递的方案是固定的,因为所有 \(A_i\)(每堆数量)最终都要到达 \(\overline{A}\),那么只要不做冗余步骤,忽略 \(X_1\)(无法左传),那么剩下就有递推关系 \(X_i=\overline{A}-(A_{i-1}-X_{i-1})\implies X_i-X_{i-1}=\overline{A}-A_{i-1}\),那么 \(X_i=X_1+(i-1)\overline{A}-\sum_{j=1}^{i-1}A_j\)。直接做即可,答案就是 \(\sum_{i=2}^n [X_i\neq 0]\),这里代码是直接用了第一条式子。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=105;
int n,a[N],x[N],S,ans;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),S+=a[i];
    S/=n;
    for(int i=2;i<=n;i++){
        x[i]=S-(a[i-1]-x[i-1]);
        ans+=(x[i]!=0);
    }
    printf("%d",ans);
    return 0;
}

拓展:P2512 [HAOI2008] 糖果传递

这题不同的地方在贡献计算以及它是一个环。这时候我们考虑像上一题一样列出 \(X_i\) 的递推式,发现其实大差不差,不同的地方是我们要考虑 \(X_1\) 并非一直为 \(0\),而是可变的,它的影响会影响到所有的 \(X_i\)。根据题目,不妨令 \(C_i=(i-1)\overline{A}-\sum_{j=1}^{i-1}A_j\implies X_i=X_1+C_i(i\neq 1)\),我们计算费用的公式是 \(\sum_{i=1}^n|X_i|=\sum_{i=1}^n|X_1+C_i|=\sum_{i=1}^n|X_1-(-C_i)|\),显然这是一个数轴找最小值的问题,取近似中位数即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+5;
int n,a[N];
LL S,C[N],X1;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),S+=a[i];
    S/=n;
    for(int i=2;i<=n;i++)
        C[i]=C[i-1]+a[i-1]-S;
    sort(C+1,C+1+n);
    X1=C[(n+1)/2];
    LL ans=0;
    for(int i=1;i<=n;i++)
        ans+=abs(X1-C[i]);
    printf("%lld",ans);
    return 0;
}

变式:将上述费用改为传递次数,即 \(\sum_{i=1}^n [X_i\neq 0]\),那么只需要找到 \(C\) 中的众数即可。

字符串 trick

  • 双串多模匹配转单串多模匹配(加入特殊字符),详见CSP-S 2025 T3

二分 trick

  • \(0/1\) trick,把图上博弈问题通过二分转化为 \(0/1\) 取值问题。
posted @ 2025-05-19 16:37  TBSF_0207  阅读(25)  评论(0)    收藏  举报