目录
动态规划
背包问题
01背包问题
思路
动态规划:原问题分解为相对简单的子问题的方式求解复杂问题的方法
1状态表示(集合,属性)
2状态转移方程(状态计算,集合划分过程)
3边界确定
i:前i个物品,j:背包目前总体积。所有从前i个物品中选,且总体积不超或j的选法(集合)
f[i][j]:总价值
1.不放入第i个物品:f[i][j]=f[i-1][j]
2.放入第i个物品:f[i][j]=f[i-1][j-v[i]]+V[i]
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+V[i])
边界:j>=v[i]
i=1;i<=N;i++
j=0;j<=V;j++
if(j>=v[i])
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+V[i]);
else f[i][j]=f[i-1][j];
或
f[i][j]=f[i-1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
int res=0;
for(int i=0;i<=V;i++)
res=max(res,f[N][i]);
printf("%d\n",res);
或
printf("%d\n",f[N][V]);
滚动数组优化:
让j从大到小遍历
完整代码
//暴力
#include<stdio.h>
int N,V;
const int n=1010;
int v[n],w[n];
int f[n][n];
int max(int a,int b)
{
if(a>=b)
return a;
else return b;
}
int main()
{
int max(int a,int b);
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++)
{
scanf("%d %d",&v[i],&w[i]);
}
int res=0;
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
{
// if(j>=v[i])
// f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
// else f[i][j]=f[i-1][j];
f[i][j]=f[i-1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
// for(int i=0;i<=V;i++)
// res=max(res,f[N][i]);
// printf("%d\n",res);
printf("%d\n",f[N][V]);
return 0;
}
//优化
#include<stdio.h>
int N,V;
const int n=1010;
int f[n];
int v[n],w[n];
int max(int a,int b)
{
if(a>=b)
return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++)
{
scanf("%d %d",&v[i],&w[i]);
}
for(int i=1;i<=N;i++)
for(int j=V;j>0;j--)
{
if(j>=v[i])
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
// int res=0;
// for(int j=0;j<=V;j++)
// res=max(res,f[j]);
// printf("%d\n",res);
printf("%d\n",f[V]);
return 0;
}
完全背包问题
思路
完全背包:每个物品可放入多次
第i个不放入:f[i][j]=f[i-1][j]
第i个放入1次:f[i][j]=f[i-1][j-v[i]]+w[i]
第i个放入2次:f[i][j]=f[i-1][j-2*v[i]]+2*w[i]
……
第i个放入k次:f[i][j]=f[i-1][j-k*v[i]]+k*w[i]
f[i][j] =max(f[i-1][j], f[i-1][j-v[i]]+w[i], f[i-1][j-2*v[i]]+2*w[i],……,f[i-1][j-k*v[i]]+k*w[i])
f[i][j-v[i]]=max(f[i-1][j-v[i]],f[i-1][j-2*v[i]]+w[i],f[i-1][j-3*v[i]]+2*w[i],……,f[i][j]=f[i-1][j-(k+1)*v[i]]+k*w[i])
so
f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])
优化:
j=v[i];j<=V;j++ //边界:j>=v[i]
f[j]=max(f[j],f[j-v[i]]+w[i])
01背包: f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]) //优化从大到小
完全背包:f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]) //优化从小到大
完整代码
//暴力
#include<stdio.h>
int N,V;
const int n=1010;
int v[n],w[n];
int f[n][n];
int max(int a,int b)
{
if(a>=b)
return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++)
scanf("%d %d",&v[i],&w[i]);
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
{
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
// if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]); //
// else f[i][j]=f[i-1][j];
}
printf("%d\n",f[N][V]);
return 0;
}
//优化
#include<stdio.h>
int N,V;
const int n=1010;
int v[n],w[n];
int f[n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++) scanf("%d %d",&v[i],&w[i]);
for(int i=1;i<=N;i++)
for(int j=v[i];j<=V;j++) f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d\n",f[V]);
return 0;
}
多重背包问题
思路
多重背包:第i个物品最多用s[i]个
01背包的扩展
状态表示:第i个物品可放入0,1,2...s[i]个
k:0-s[i]
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i]...f[i-1][j-k*v[i]]+k*w[i])
k从0开始循环,f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])
优化
j从大到小,因为上式都是f[i-1]
f[j]=max(f[j],f[j-k*v[i]]+k*w[i])
边界:k=0;k<=s[i]&&k*v[i]<=j;k++
完整代码
//暴力
#include<stdio.h>
int N,V;
const int n=110;
int v[n],w[n],s[n];
int f[n][n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++) scanf("%d %d %d",&v[i],&w[i],&s[i]);
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
{
// f[i][j]=f[i-1][j];
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
printf("%d\n",f[N][V]);
return 0;
}
//优化
#include<stdio.h>
const int n=110;
int N,V;
int v[n],w[n],s[n];
int f[n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++) scanf("%d %d %d",&v[i],&w[i],&s[i]);
for(int i=1;i<=N;i++)
for(int j=V;j>=0;j--)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
printf("%d\n",f[V]);
return 0;
}
二进制优化思路
这二进制优化把人看傻了,发明二进制的人是个天才
任何数都能用二进制表示
8:1,2,4 8 >=2^3(8) 取2^2(4) ,c(2)
10:1,2,4 10 >=2^3(8) 取2^2(4) ,c(4)
200:1,2,4,8,16,32,64,73 200>=2^7(128) 取2^6(68),c(73)
将s个第i个物品 拆分为 n组(二进制表示)个第i个物品,将前n-1个i物品 变为第i个物品之前的单个物品
所以就转化为01背包问题
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s=s-k;
k=k*2;
}
if(s>0)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
优化代码
#include<stdio.h>
int N,V;
const int n1=25000,n2=2010;
int v[n1],w[n1];
int f[n2];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
int cnt=0;
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++)
{
int a,b,s;
scanf("%d %d %d",&a,&b,&s);
int k=1;
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s=s-k;
k=k*2;
}
if(s>0)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
for(int i=1;i<=cnt;i++)
for(int j=V;j>=v[i];j--) f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d",f[V]);
return 0;
}
分组背包问题
思路
分组背包:有N组物品,每组只能选一个
状态计算:
第i组选第一个:f[i][j]=f[i-1][j]
第i组选第k个:f[i][j]=f[i-1][j-v[i,k]]+w[i,k]
s[i]:每组个数(表示个数,注意边界)
完整代码
#include<stdio.h>
int N,V;
const int n=110;
int s[n];
int v[n][n],w[n][n];
int f[n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&V);
for(int i=1;i<=N;i++)
{
scanf("%d",&s[i]);
for(int j=0;j<s[i];j++) scanf("%d %d",&v[i][j],&w[i][j]);
}
for(int i=1;i<=N;i++)
for(int j=V;j>=0;j--)
for(int k=0;k<s[i];k++)
{
if(j>=v[i][k])
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
printf("%d\n",f[V]);
return 0;
}
线性DP
数字三角形
最长上升子序列
思路
基础版
f[i]:以第i个数结尾的子序列的集合,f[i]表示子序列的长度
f[i]=f[i-1]+1
i=1;i<=N;i++
f[i]=1
j=1;j<=i;j++
if(a[j]<a[i])
f[i]=max(f[i],f[j]+1)
优化版
优化版得先看一遍二分,整数二分边界问题真受够了
子序列长度从1开始,长度为1,长度为2...每个长度下以数字i结尾的子序列,只存i最小的子序列,存在q[i]中
//因为如果一个数能排在3后面,就一定能排在1后面。当长度一定时,子序列结尾的数字 ,只要记录一个数字最小的子序列
a[i]:数字
遍历a[i],二分法找a[i]可以跟在哪个数字(q[i])后面
int len=0,l=0,r=len;
if(q[mid]<a[i]) l=mid //答案在左边<a[i]的部分,但是不包含a[i]
else r=mid+1
len=max(len,r+1) //r是二分答案,是最大<a[i]的数,a[i]应该接在r后面,a[i]一旦接在r后面,子序列长度+1,以a[i]为结尾的最长子序列应属于r+1,len是右边界
完整代码
//基础版
#include<stdio.h>
int N;
const int n=1010;
int a[n],f[n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d",&N);
for(int i=1;i<=N;i++) scanf("%d",&a[i]);
for(int i=1;i<=N;i++)
{
f[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
f[i]=max(f[i],f[j]+1); //卧草好聪明的办法
}
}
int res=0;
for(int i=1;i<=N;i++)
res=max(res,f[i]);
printf("%d\n",res);
return 0;
}
//优化版
#include<stdio.h>
int N;
const int n=100010;
int a[n],q[n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d",&N);
for(int i=0;i<N;i++) scanf("%d",&a[i]); //二分法只能从0开始循环
for(int i=0;i<N;i++) q[i]=a[i];
int len=0;
q[0]=-2e9; //先给q[0]赋值,第一次for只会用到q[0],
for(int i=0;i<N;i++)
{
int l=0,r=len;
while(r-l>0)
{
int mid=(l+r+1)/2;
if(q[mid]<a[i]) l=mid; //第一次循环q[mid]=q[0],已经赋值过了
else r=mid-1;
}
len=max(len,r+1);
q[r+1]=a[i]; //这一步就相当于给q[]赋值了
}
printf("%d\n",len);
return 0;
}
最长公共子序列
思路
状态表示
f[i][j]:第一个序列a[]前i个数字的子序列、第二个子序列b[]前j个数字的子序列、构成的所有公共子序列(集合)的长度最大值
状态计算
用01表示最长公共子序列是否包含a[i]、b[j]
00:f[i][j]=f[i-1][j-1]
01:f[i-1][j]
//01:子序列不包含a[i],包含b[j]
//f[i-1][j]:a[]中前i-1个数字的子序列,b[]中前j个数字的子序列,构成的公共子序列(集合)的长度最大值
/*所以f[i-1][j]一定不包含a[i],有包含b[j]也有不包含b[j]
所以f[i-1][j]>01*/
//max(a,b,c)=max(max(a,b),max(b,c))
10:f[i][j-1] //同理
11:f[i-1][j-1]+1
//+1而不是+2,因为只有a[i]==b[j]才会出现11
i=1;i<=N
j=1;j<=M
{
f[i][j]=max(f[i][j-1],f[i-1][j])
if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1)
!!!注意
scanf("%c",&a[i])会读取空格,换行符
只能用scanf("%s",a+1)
完整代码
#include<stdio.h>
int M,N;
const int n=1010;
char a[n],b[n];
int f[n][n];
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
int main()
{
scanf("%d %d",&N,&M);
// for(int i=1;i<=n;i++) scanf("%c",&a[i]);
// for(int i=1;i<=M;i++) scanf("%c",&b[i]);
// scanf%c会在每次读取字符时保留换行符或空白字符,应该直接读取整个字符串,而不是逐个字符读取
scanf("%s",a+1);
scanf("%s",b+1);
for(int i=1;i<=N;i++)
{
for(int j=1;j<=M;j++)
{
f[i][j]=max(f[i-1][j],f[i][j-1]);
if(a[i]==b[j])
f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
}
printf("%d\n",f[N][M]);
return 0;
}
最短编辑距离
思路
状态表示:集合:方法。把A[i]序列变为B[j]序列需要的所有方法的集合
状态计算:1.删:f[i-1][j]+1
2.增:f[i][j-1]+1
3.改:f[i-1][j-1]+1
初始化
for (int i = 0; i <= n; i++) f[i][0] = i;
for (int i = 0; i <= n; i++) f[0][i] = i;
3.if(a[i] == b[i]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
完整代码
#include<stdio.h>
const int N = 1010; //这行必须放在char A[N]前,,我记得以前不用啊
int n, m;
char A[N], B[N]; //注意char类型
int f[N][N];
int min(int a, int b)
{
if(a <= b) return a;
else return b;
}
int main()
{
scanf("%d %s", &n, A + 1);
scanf("%d %s", &m, B + 1);
for (int i = 0; i <= n; i++) f[i][0] = i;
for (int j = 0; j <= m; j++) f[0][j] = j;
for (int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if(A[i] == B[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}
基础算法
二分法
用y总的话来说,整数二分的边界问题,是真TM蛋疼
思路
//整数
i=0;i<n;i++
q[i]
int l,r;
l=0,r=n-1
//左边界
while(l<r)
mid=(l+r)/2
if(a[mid]>=k) r=mid //每次选答案所在的区间,区间一定要将答案覆盖掉
else l=mid+1
printf()
if()
//右边界
else {
int l=0,r=n-1
while()
mid=(l+r+1)/2
if(a[mid]<=k) l=mid;
r=mid-1
printf()
//浮点数
取后六位:while(r-l>1e-8)
浮点数二分比整数二分舒服多了
完整代码
//整数
#include<stdio.h>
int n,q;
const int N=100010;
int a[N];
int main()
{
scanf("%d %d",&n,&q);
// for(int i=1;i<=n;i++)
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
while(q--)
{
int k;
scanf("%d",&k);
int l,r;
// l=1,r=n;
l=0,r=n-1;
while(l<r)
{
int mid=(l+r)/2;
if(a[mid]>=k) r=mid;
//每次选答案所在的区间,区间一定要将答案覆盖掉
else l=mid+1;
}
if(a[l]!=k) printf("-1 -1\n");
else{
printf("%d ",l);
// int l=1,r=n;
l=0,r=n-1;
while(l<r)
{
int mid=(l+r+1)/2;
if(a[mid]<=k) l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
}
return 0;
}
//浮点数
//求一个数的三次方根
#include <stdio.h>
int main()
{
double n;
scanf("%lf",&n);
double l=-10000.0,r=10000.0;
while(r-l>1e-8)
{
double mid=(l+r)/2;
if(mid*mid*mid>=n) r=mid;
else l=mid;
}
printf("%lf\n",l);
return 0;
}