数学
1.先分析
2.打表找规律,即暴力找前面的一部分,总结公式,规律
需要多总结一些公式
n和m最大不能组成的数字:nm-n-m
蚂蚁相撞会掉头,相当于一直向一个方向走,不掉头
动态规划
和递推很像,从实际意义出发,比如斐波那契数列
列状态转移;然后确定初始化
01背包问题
//二维数组
#include<iostream>
using namespace std;
const int N=1005;
int f[N][N];
int v[N],w[N];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
if(v[i]<=j)
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[n][m]<<endl;
return 0;
}
//对代码做等价变换
f[j-v[i]]实际是二维数组里的f[i][j-v[i]],但我们要用的是f[i-1][j-v[i]]
//一维数组
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m]<<endl;
线性dp
数字三角形:路线问题
f[i][j]:
集合:从底向上走到(i,j)的集合(表示一类路线)
属性:最大值
#include<iostream>
#include<algorithm>
using namespace std;
const int N=510;
int f[N][N],n;
int main(){
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)cin>>f[i][j];//从底走到(i,j)的最大路径
for(int i=n-1;i>=1;i--)
for(int j=i;j>=1;j--){
f[i][j]+=max(f[i+1][j],f[i+1][j+1]);//由左边和右边分别走上来
}
cout<<f[1][1]<<endl;//最终答案
return 0;
}
dp问题 从集合的角度来看,清晰一些
状态表示:用什么状态表示一个集合,
状态计算:集合的划分(把集合再分为若干更小的类计算)(用当前(i,j)与(i+-1,j+-1)的逻辑关系)
f[i][j]表示的集合(一类)和属性(看这个问题问的是什么最大值,最小值,个数)
集合表示
最长上升子序列
dp时间复杂度计算(总状态数量*每个状态计算所需时间)
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n,a[N],f[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
f[i]=1;//注意初始化
for(int j=i-1;j>=1;j--)
if(a[i]>a[j])f[i]=max(f[i],f[j]+1);
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(f[i],ans);
cout<<ans<<endl;
return 0;
}
最长上升子序列2
1的冗余优化
每种长度的上升子序列的最后一个最小的值是多少
先二分找到每个数应该为哪个子序列长度的最后一个值,
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int a[N],q[N],n,len=0;
int binary(int x){
int l=0,r=len;
while(l<r){
int mid=l+r+1>>1;
if(q[mid]>x)r=mid-1;
else l=mid;
}
return l;
}
int main(){
cin>>n;
q[0]=-2e9;
for(int i=1;i<=n;i++)cin>>a[i];
//q[1]=a[1];
for(int i=1;i<=n;i++){
int x=binary(a[i]);
q[x+1]=a[i];
len=max(len,x+1);
}
cout<<len<<endl;
return 0;
}
最长公共子序列
//求最大值,最小值时,状态计算时子集的划分可以重复,但是不能遗漏
//对于
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N][N];
char a[N],b[N];
int main(){
int n,m;
cin>>n>>m>>a+1>>b+1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i]==b[j])f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i][j-1],f[i-1][j]);//至少一个不选的情况
//f[i-1][j]是包含b[j]之前的字母,但不是一定选b[j],重复了但是完全涵盖了就可以
}
}
cout<<f[n][m]<<endl;
return 0;
}
//可以先思考一下,集合里元素的总数,然后考虑怎么表示它的状态
//第一个序列的前i个字母并且在第二个序列的前j个字母中出现的子序列
//a[i],b[j]是否在集合f[i][j]中,00,01,10,11(这四项中都有重复,但是要求的是最大值,集合划分时可以重复)
//但是属性是数量时不能重复
技巧:输入字符串
char a[N];
scanf("%s",a+1);
cin>>a+1>>b+1;
区间dp
用dp前,先要证明是有限集合
每次可以处理一堆东西
一.什么是区间dp
顾名思义:区间dp就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的 最优解进而得出整个大区间上最优解的dp算法。
二.核心思路
既然让我求解在一个区间上的最优解,那么我把这个区间分割成一个个小区间,求解每个小区间的最优解,再合并小区间得到大区间即可。所以在代码实现上,我可以枚举区间长度len为每次分割成的小区间长度(由短到长不断合并),内层枚举该长度下可以的起点,自然终点也就明了了。然后在这个起点终点之间枚举分割点,求解这段小区间在某个分割点下的最优解。
一般先枚举区间长度,再枚举区间左端点(起点), 然后对每个区间进行划分n种可能,枚举,求解小区间的最小值
//区间长度从2开始,因为区间长度为1时,不需要操作合并
for(int len=2;len<=n;len++)
//先枚举长度
for(int i=1;i+len-1<=n;i++){
//i是起点
int l=i,r=i+len-1;
//区间(l,r)表示出来了
//区间以位置k再分两堆,找最小值
for(int k=l;k<r;k++){//k!=r因为要成一堆,f[r+1][r]
f[l][r]=min(f[l][k]+f[k+1][r]+something前缀和之类的,f[l][r]);
}
}
石子合并
n*n*n
计数类DP
所谓计数类DP就是常说的统计可行解数目的问题,区别于求解最优解,此类问题需要统计所有满足条件的可行解,而求解最优值的DP问题往往只需要统计子问题时满足不漏的条件即可,但是计数类DP需要满足不重不漏的条件,是约束更高的。
最常见的计数类DP问题就是上楼梯的问题,我们要求解此类问题一个重要的点就是如何划分子问题,然后做到不重不漏,大部分情况下我们想到的方法,同一个解可能会被多次统计,这是不合理的。此类问题也常常与组合数结合到一起,我们可能需要用到数学中组合数的概念,所以如何快速实现组合数,也要掌握好。
对于如何不重不漏地计数,我觉得一个很重要的点就是定序,也即制定一个数据排列的顺序,使得后面的数据不会影响前面的统计结果。
整数划分
由完全多重背包推公式
数位统计DP
计数问题
状态压缩dp
状态压缩DP一般是基于二进制进行的(需要用到位运算)
状态压缩DP一般分为两类:
①基于连通性DP(棋盘式)
②集合式(表示每一个元素是否在集合中)
连通性DP
技巧:二进制运算
1.i>>j&1 显示的是i的二进制表示的第j位是否是1
2.cnt&1 判断cnt的奇偶性,奇数的二进制位最后一位一定是1
3.int i=0,i<1<<n,i++,2的n次方大小
4.& | 运算
5.二进制数可以表示选的路径
全局变量,静态变量默认开在堆里
集合式
这两个点可以唯一确定我们现在的状态是什么
1.哪些点被用过
2.目前停在哪些点上
减法优先级比右移优先级大
树形DP
0. 定义
树形DP,又称树状DP,即在树上进行的DP,是DP(动态规划)算法中较为复杂的一种。
1. 基础
令f [ u ] = f[u]=~f[u]= 与树上顶点u uu有关的某些数据,并按照拓扑序(从叶子节点向上到根节点的顺序)进行DP \text{DP}DP,确保在更新一个顶点时其子节点的dp值已经被更新好,以更新当前节点的DP \text{DP}DP值。为方便计算,一般写成dfs的形
void dfs(int v) { // 遍历节点v
dp[v] = ...; // 初始化
for(int u: G[v]) { // 遍历v的所有子节点
dfs(u);
update(u, v); // 用子节点的dp值对当前节点的dp值进行更新
}
}
2. 树上背包
在基本算法之上,树形dp还可以用于树上背包问题。
3. 换根 DP
换根DP,即为不知道根结点时使用的一种树形DP,时间复杂度一般为O ( N ) \mathcal O(N)O(N)。