dp
通用技巧
1.见到求绝对值就可以拆,直接变成对整体答案的贡献。转移上可以考虑按顺序转移,设一维状态为前i个未匹配的个数,转移的时候枚举向大或小转移;或者用状压表示大于或小于的状态。
2.在子序列计数的问题中,某些情况下可以用前缀和或bitset优化。
3.如果要求不能重复转移的问题,不妨使用填表法,并设一个辅助数组,强制某个点只能转移一次。不要先去重再计算,十分麻烦!!
4.刷表法一般来说不容易写错,但在某些时候是不好转移的。在对于有特殊限制并加入辅助状态的时候,填表法更能表现出某个状态是否被更新到,然后去更新后面。
5.高精除低精类似于模拟竖式,而高精除高精就只能一点一点去减了。注意判断除0情况。
6.在需要分别枚举如左右贡献a,b这两个变量时,我们不妨枚举d=b-a,推出其组合贡献,可以使用 $$ \sum_{b=0}^{min(m,a)} C_b^a * C_{m-b}^{n-a} = C_m^n $$
7.对于 \(N^2\) 转移的dp,可以考虑转化为网格,观察其图形转移性质,从而化为更低维dp。
8.对于某些dp的转移,可以形象化理解,比如考虑在一个空间平面上,如何给下一层转移。或者通过最短路转移等等。
9.对于计算本质不同的子序列,可以 \(O(n^2)\) \(dp\),设 \(dp_i\) 表示以i结尾的本质不同的子序列个数。转移时钦定只对它靠一侧的匹配转移。
可以优化到 \(O(n)\) ,即设 \(dp_i\) 表示以 <=i结尾的本质不同的子序列个数。但是两种写法一定要分清楚不要混了。
10.对于只有两三种不同颜色去排列的方案,dp时一定要记得设上一次出现的位置为i,j,(k),再根据下一个放什么去转移!
11.dp时一定要看空间上界,有些维可能只有log级别!(笑死了dp写对了看不出来最多log次
12.开dp数组时不要信任评测机的空限,如果卡到顶了一定要滚动数组优化。
13.求lr区间覆盖的某些问题,考虑转化为二维平面,有时可以用二维差分/前缀和去做。
14.见到形如 \(\sum_{i=1}^{k} cnt_i^2\),直接转化为组合意义:选出两条满足限制的同色链的方案数。类似的,很多题目让求一个神秘东西的和,都是直接用组合意义转换。
状压dp
直接根据数据范围观察,小技巧是枚举子集
for(int s=t;s;s=(s-1)&t)
枚举子集的子集的时间复杂度是 \(O(3^n)\) ,可以把式子展开使用二项式定理证明。推广:枚举k次子集的时间复杂度是 \(O((k+1)^n)\) .
还有一个技巧吧,用子集的容斥原理的时候,直接减是会重复的。钦定一个点(比如1),把子集划分成含1的连通块和其他部分,再去枚举就能保证不重不漏了。
for(int i=1;i<=tmp;i++){
f[i]=g[i];
for(int j=(i-1)&i;j;j=(j-1)&i){
if(!(j&1)) continue;
f[i]=(f[i]-f[j]*g[i^j]%mod+mod)%mod;
}
}
计数dp
一般指求一个集合S的大小,并且常常范围非常大需要取模。
如果我们能将S分成若干无交的子集,那么S的元素个数就等于这些部分的元素个数和。
与最优化dp的异同:同,都是在一个范围内求一个大小值或最优值,这个值通过对范围内的所有元素做一次处理和整合得到;异,最优化dp我们只需要求满足这个范围的最值,即部分的并为范围,而计数dp我们则需要把范围分成若干个不交的部分。
计数dp的精髓就在于状态设计,而且多是多维dp,经常有很多想不到的神仙状态。
比如经常设计成0/1表示在左右或是否满足条件,包括设计成还需要多少才能满足条件这种([PA2021] Od deski do deski)。
预设型dp
我的理解是把需要求的限制直接加到dp状态中转移。
一般来说先想到设f[i][j](如,枚举到i位,放了j),但是发现有后效性、无法转移,而恰好题目又给了一个在每次转移都需要计算或满足的条件,那么不妨加入那一维条件。
比如求满足划分成若干段的方案,最后一维是段数;求达到奇怪度的方案数,最后一维是奇怪度([ABC134F] Permutation Oddness)。一般是数据范围小的高维dp。
(这样的dp一般最终求的是满足某个限制的方案数)
区间dp
我到现在都觉得区间dp很难……形式过于多样就很难想到。只能积累一些方法。
断环为链。
除了dp[l][r]之外,可以加入比如每个的对l,r的值的限制。尤其是对于删除添加色块的题目。
关于如何注意到要使用区间dp:多推性质,正想反想,时间倒流,删改成加等等。
如果状态实际不多但正常转移会tle或mle,可以考虑记忆化搜索。
概率与期望(坑)
常用结论:期望操作次数=1/得到目标的概率
(期望)e=(概率)P*(每次贡献)w
第二个结论常常不仅用于求期望,有时还用于在抽象计数问题中每种情况的概率和期望比较好求时,求对于所有方案的某种操作次数总和( \(\sum w\) )
一种特殊情况是在游走的时候还可能停在当前点,那么如何计算到达当前点的概率?先找定值,即从每个点转移(到别的点或者停留在本点)的概率一定是1,而最终在某个城市爆炸的概率是我们所要求的。其实就很像方程了,把在某个城市爆炸的概率设为x,转移到该城市的概率是好表示的,我们可以结合高斯消元去做。
例 [USACO Hol10] 臭气弹
//这个题最离谱的就是如何求无限循环的概率?? (还都是双向边
//或者说根据题中给出的在某个城市结束的概率之和就是需求的概率,这样考虑列方程?
//这种概率的问题是真的不好想!有点抽象,只知道每个xi,如何构造方程?
//似乎先找单位1比较好,列出的方程为单位x的概率减掉每个能到达该边的点的转移概率等于零!!
//还有是从1开始,故1特殊为p/q,本来初始在这里就有值!!
//注意每个可转移的点还要除以它的入度!!!这样才是最终的概率
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-10;
int n,m,p,q,c[305][305],d[305];
double a[305][305];
int main(){
scanf("%d%d%d%d",&n,&m,&p,&q);
double x=1.0*p/q;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
c[u][v]=c[v][u]=1;
d[u]++,d[v]++;
}
a[1][n+1]=x;
for(int i=1;i<=n;i++){
a[i][i]=1.0;
for(int j=1;j<=n;j++){
if(c[i][j]) a[i][j]=-(1.0-x)/d[j];
}
}
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
if(fabs(a[j][i])>eps){
swap(a[j],a[i]);
break;
}
}
for(int j=n+1;j>=i;j--) a[i][j]/=a[i][i];
for(int j=i+1;j<=n;j++){
for(int k=n+1;k>=i;k--){
a[j][k]-=a[i][k]*a[j][i];
}
}
}
for(int i=n-1;i>0;i--){
for(int j=i+1;j<=n;j++){
a[i][n+1]-=a[j][n+1]*a[i][j];
}
}
for(int i=1;i<=n;i++){
printf("%.9lf\n",a[i][n+1]);
}
return 0;
}
dp套dp
[TJOI2018] 游园会 是模版。
大概就是发现这个题需要维护一个lcs=i的限制,然而我们并不能直接得到lcs,lcs也是一种对整体状态dp出来的结果。而我们又发现求出来lcs的数组实际上只会两两相差1,也就是可以使用状压去维护。考虑状压维护的正确性,每次更新都是在之前更新过的dp数组的基础上新加入一个字符,相当于我们普通二维dp拆解开,一点一点更新。
然后lcs的dp可以预处理出来,再状压dp转移方案。如果理解清楚好像也还好。
还有一道例题[ZJOI2019] 麻将。还没有写……
树形dp
一般来说就是一维表示当前节点的状态,如果有需要,再加几维表示题目限制。
树上背包,在每个节点初始化状态时一定是只包含当前节点,然后去枚举子树和容量、分配,每次一般是方案相乘或取max,一定是对于当前x及其已经合并的子树和当前要合并的y子树操作。写状态转移的时候要想清楚。
关于树形背包的复杂度,看着是四个for实际上可以证明是O(n3),看着是三个for实际可以证明是O(n2).因为有一个枚举子树的操作,那么我们只看枚举的x已有子树和y子树中的每对点,发现实际上每对点只在lca处被计算。([HNOI2015] 实验比较)
up and down,同样是经典套路。实际上是换根dp,一般来说up时从下向上遍历f[x]表示x子树内的贡献,down时从上向下遍历g[x]表示以x为根的全树贡献,g[1]=f[1].
矩阵快速幂加速dp
同样的,看数据范围。
一般来说,只要转移的条件不变,且数据范围比如n很大,并且转移状态又很小并且可以被矩阵所表示,那大概率是矩阵快速幂加速dp。(还有一种情况是转移状态并不需要依赖大范围的n,而是通过较小范围的比如m去设计dp,把n作为转移的参数。)
我觉得这个最难的就是矩阵的初始化和答案的输出,一定要把状态想清楚了,到底初始化后是不是需要转移n次,还是n-1次;输出的的答案是求和还是其中的某个值。
需要注意的点:first.矩阵乘的单位1是除主对角线为1外全0的矩阵
second.封装的时候一定记得清空,否则那个矩阵设出来实际上是有值的,影响计算
struct node{
int a[30][30];
void clear(){
memset(a,0,sizeof(a));
}
node operator *(const node b){
node res;
res.clear();
for(int i=1;i<=m;i++){
for(int j=1;j<=m;j++){
for(int k=1;k<=m;k++){
res.a[i][j]=(res.a[i][j]+a[i][k]*b.a[k][j]%mod)%mod;
}
}
}
return res;
}
}x,ans;
基环树(树形dp
最常见条件:n个点,n条边(注意题目给的条件是否联通,可能是森林
基环内向树:每个点只有一条出边
基环外向树:每个点只有一条入边
处理手法:找环,断边,跑树形dp
void circle(int x,int p){
vis[x]=1;
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if((i^1)==p) continue;
if(vis[y]){
xx=x,yy=y,k=i;
continue;//不能return,要把联通块找完
}
circle(y,i);
}
}
插头dp/轮廓线dp
就是状压的一种思路,把两行状态的枚举转化为逐格dp,感觉转移好像有一条轮廓线一样,把转移变为O(1)
一般来说是一种用于维护连通性的dp,但是其实我知识点还没学到插头dp。。。
高维前缀和/sosdp(子集dp)
用于对于集合的每个子集求解一些问题,枚举子集的复杂度就到了\(O(2^n)\),没法逐个计算,而高维前缀和可以做到 \(O(n*2^n)\)
理解方面,和容斥原理有关,考虑我们按照一定顺序枚举,形象理解为每次枚举从头到尾某个元素不选,而该元素后面都选,又按照由小到大转移,每个全部包含的区间都经历了同样的枚举,从而保证是不重复的
另外,与之对应的还有高维后缀和,但通过将所有东西取补集,就变成了高维前缀和,可以类似处理。以及还有高维差分,循环反向,加号变减号即可
其实我也还是不是很理解高维后缀和等一系列,看 奆佬blog
//要求最多O(log)求出对于每个子集中满足条件的个数
//如果不要求时间复杂度,转化成求一段区间内lst在l之前的,莫队差不多可以写
//对于这一类求集合中子集的问题,理解方面,和容斥原理有关,考虑我们按照一定顺序枚举,并计算补集每次删掉的元素,从而保证转移是不重复的
#include<bits/stdc++.h>
using namespace std;
int n=24,m,dp[1<<24],ans;
int main(){
scanf("%d",&m);
for(int i=1;i<=m;i++){
int s=0;
for(int j=1;j<=3;j++){
char c;
scanf(" %c",&c);
if(c>='y') continue;
s|=(1<<(c-'a'));
}
dp[s]++;
}
for(int i=0;i<n;i++){
for(int j=0;j<(1<<n);j++){
if(j&(1<<i)) dp[j]+=dp[j^(1<<i)];
}
}
for(int i=0;i<(1<<n);i++) ans^=(m-dp[i])*(m-dp[i]);
printf("%d",ans);
return 0;
}
贪心 (乱入
Q:为什么把贪心和dp放一起呢?
A:贪心题中有时会借用dp排除一些情况([JOI 2022 Final]让我们赢得选举),而dp的最优策略选则有时需要使用贪心思想。
贪心,指在问题求解中总是做出在当前看来最好的选择,从局部最优解出发,得到整体最优解
重点在于贪心策略的选择
一般来说,先对问题建立数学模型,然后拆分问题为若干个子问题,从子问题最优解得到整体最优解
其实在很多时候,贪心策略的证明是很难的,但要发散想象力,敢想敢写
比如:
贪心常见设问是最大值最小和最小值最大(怎么和二分一样,当然二分中的check很多都是用了贪心的思想
考虑交换顺序的问题时,用相邻交换法,只想两个的情况,通过题目给的条件列式子分析,推出排序策略,很多都是按照max/min、和、差排序;注意一定先只想两个,但后面也一定要扩展到整体情况的(有时候贪心策略不具有传递性
johnson算法:对于推出来是min(ai,bj)<min(aj,bi)则i比j优的情况,直接这么排不具有传递性,但是我们可以知道a小的尽可能放前面,b小的尽可能放后面。这个式子可以直接拆开分五种情况讨论(含等于,但等于时可以随便排),于是我们得到了一个正确的排序方式。令d=(ai<=bi)?-1:1,先按照d排序,d同为-1时按照a升序排列,同为1时按b降序排列。