一、背包问题
AcWing 2. 01背包问题
思路:
按照最后一个物品(第i个物品)选还是不选划分集合
代码一 朴素做法:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=1010;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
v[i]=nextInt();
w[i]=nextInt();
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]= Math.max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
pw.println(f[n][m]);
pw.close();
}
}
代码二 空间优化:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=100010;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[]=new int[N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
v[i]=nextInt();
w[i]=nextInt();
}
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
f[j]= Math.max(f[j],f[j-v[i]]+w[i]);
}
}
pw.println(f[m]);
pw.close();
}
}
AcWing 3. 完全背包问题
思路:
按照第i个物品选几个划分集合,k表示第i个物品选k个
状态转移方程:f(i,j)=f(i-1,j-k*v[i])+k*w[i] (k=0,1,2····)
状态转移方程等价变形,可消去一层循环
因此,状态转移方程:f(i,j)=f(i-1,j-k*v[i])+k*w[i]=f(i,j-v[i])+w[i] 需满足 j>=v[i] 条件
k=0时一定合法,因此f(i,j)=f(i-1,j-k*v[i])+k*w[i]=max(f(i-1,j),f(i,j-v[i])+w[i])
代码一 朴素做法:
//完全背包:每件物品有无限个
//状态转移方程:f(i,j)=f(i-1,j-k*v[i])+k*w[i] (k=0,1,2··) 第i个物品选k个
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=1010;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
v[i]=nextInt();
w[i]=nextInt();
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k*v[i]<=j;k++){
f[i][j]= Math.max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
pw.println(f[n][m]);
pw.close();
}
}
代码二 优化掉一层循环:
//完全背包:每件物品有无限个
//状态转移方程:f(i,j)=f(i-1,j-k*v[i])+k*w[i]=f(i,j-v[i])+w[i]
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=1010;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
v[i]=nextInt();
w[i]=nextInt();
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];//k=0时
if(j>=v[i]) f[i][j]= Math.max(f[i][j],f[i][j-v[i]]+w[i]);
}
}
pw.println(f[n][m]);
pw.close();
}
}
代码三 进一步空间优化:
看状态转移方程:
用到本层的需从小到大遍历(完全背包问题) f(i,j)=max(f(i-1,j),f( i,j-v[i])+w[i])
用到上一层的需从大到小遍历(0-1背包问题) f(i,j)=max(f(i-1,j),f( i-1,j-v[i])+w[i])
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=1010;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[]=new int[N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
v[i]=nextInt();
w[i]=nextInt();
}
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
f[j]= Math.max(f[j],f[j-v[i]]+w[i]);
}
}
pw.println(f[m]);
pw.close();
}
}
AcWing 4. 多重背包问题
思路:
由于题目所给范围只有100,因此三层循环可以ac
只是在完全背包问题朴素版写法上加了个数限制 k<=s[i],也是按照第i个物品选几个划分集合
代码:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=110;
static int v[]=new int[N];
static int w[]=new int[N];
static int s[]=new int[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
v[i]=nextInt();
w[i]=nextInt();
s[i]=nextInt();
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=s[i] && k*v[i]<=j;k++){
f[i][j]= Math.max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
pw.println(f[n][m]);
pw.close();
}
}
AcWing 5. 多重背包问题 II
思路:
本题所给n范围为1000,因此三层循环会TLE,考虑优化
将一个物品的物品数s拆分为logs份,每一份都是2^0,2^1····,则将每一份进行组合,一定可以得到任意数量的物品;
因此将每个物品数s[i]进行拆分,拆分为2^0,2^1······然后对每一份做一遍0-1背包问题即可;
注意:
若不空间优化,则需开辟12010*12010个int,大于64M,会MLE
代码一 朴素做法,会超内存限制,必须进行空间优化(二维变一维):
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=12010;//每种物品有s个拆分成logs类,共1000种物品,每种最多2000,因此总个数为1000*log2000≈12000;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[][]=new int[N][N];
static int cnt=0;
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
int a=nextInt();
int b=nextInt();
int s=nextInt();
int k=1;
while (k<=s){
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0){//有剩余说明剩余数凑不出2^k
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]= Math.max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
pw.println(f[n][m]);
pw.close();
}
}
代码二 空间优化后代码 可AC:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=12010;//每种物品有s个拆分成logs类,共1000种物品,每种最多2000,因此总个数为1000*log2000≈12000;
static int v[]=new int[N];
static int w[]=new int[N];
static int f[]=new int[N];
static int cnt=0;
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
int a=nextInt();
int b=nextInt();
int s=nextInt();
int k=1;
while (k<=s){//按照2的倍数进行打包
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0){//有剩余说明剩余数凑不出2^k
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;//做一遍0-1背包问题
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
f[j]= Math.max(f[j],f[j-v[i]]+w[i]);
}
}
pw.println(f[m]);
pw.close();
}
}
AcWing 9. 分组背包问题
思路:
按照第i组物品选哪个划分集合;
第一个小区域表示不选第i组物品,第二个小区域表示选第i组物品中的第1个......以此类推,选择第i组物品的第k个.....
代码一 朴素做法:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=110;
static int v[][]=new int[N][N];
static int w[][]=new int[N][N];
static int f[][]=new int[N][N];
static int s[]=new int[N];
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
s[i]=nextInt();//读入每组的个数
for(int j=1;j<=s[i];j++){//依次读入第i组的第j个的物品
v[i][j]=nextInt();
w[i][j]=nextInt();
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=s[i];k++){//k=0,表示不选第i组物品,因为v[i][0]=0,w[i][0]=0
if(j>=v[i][k]) f[i][j]= Math.max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);//只有背包容量大于第i组的第k个物品时,才能选择该物品
}
}
}
pw.println(f[n][m]);
pw.close();
}
}
代码二 空间优化后
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=110;
static int v[][]=new int[N][N];
static int w[][]=new int[N][N];
static int f[]=new int[N];
static int s[]=new int[N];//每组的物品个数
public static void main(String args[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
s[i]=nextInt();
for(int j=1;j<=s[i];j++){//编号从1开始
v[i][j]=nextInt();
w[i][j]=nextInt();
}
}
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){//由于原状态转移方程用到了i-1层,因此需从大到小遍历
for(int k=0;k<=s[i];k++){//选每组中的第k个物品,当k=0,表示不选第i组物品,因为v[i][0]=0,w[i][0]=0;
if(j>=v[i][k]) f[j]= Math.max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
}
pw.println(f[m]);
pw.close();
}
}
二、线性DP
AcWing 898. 数字三角形
思路:
按照最后一步来自左上方还是右上方划分集合
状态转移方程:f(i,j)=max(f(i-1,j-1)+a(i,j),f(i-1,j)+a(i,j))
注意:由于数字三角形的整数可能为负数,因此需要将数字三角形初始化为负无穷
代码:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=510;
static int INF=0x3f3f3f3f;
static int a[][]=new int[N][N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
n=nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
a[i][j]=nextInt();
}
}
for(int i=0;i<N;i++) Arrays.fill(f[i],-INF);//对状态矩阵进行初始化,由于要考虑边界问题,因此将全部点赋为-INF;
f[1][1]=a[1][1];//初始化
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
f[i][j]= Math.max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
}
}
int res=-INF;
for(int i=1;i<=n;i++) res= Math.max(res,f[n][i]);//遍历最后一行找到最大值
pw.println(res);
pw.close();
}
}
AcWing 895. 最长上升子序列
思路:
数据范围为1000,可以用0(n^2)的复杂度
状态表示 集合:f(i)表示以a[i]结尾的上升子序列的集合,属性:Max
状态计算 以倒数第二个序列是a[?]划分结合,0表示不存在倒数第二个数,即只有a[i]本身;1表示倒数第二个数是a[1],即最后两个数是a[1]a[i];·····j表示倒数第二个数是a[j],即最后两个数是a[j]a[i];
状态转移方程: f(i)=max(f(j)+1) (j=0,1,2····· i-1 需满足a[j]<a[i])
初始化:f[i]=1,表示只有a[i]这一个数,对应j=0这种情况;
代码:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=1010;
static int a[]=new int[N];
static int f[]=new int[N];
public static void main(String args[]) throws IOException{
n=nextInt();
for(int i=1;i<=n;i++) a[i]=nextInt();
for(int i=1;i<=n;i++){
f[i]=1;//初始化,表示只有a[i]一个数字
for(int j=1;j<i;j++){
if(a[j]<a[i]) f[i]= Math.max(f[i],f[j]+1);
}
}
int res=0;
for(int i=1;i<=n;i++) res= Math.max(res,f[i]);
pw.println(res);
pw.close();
}
}
AcWing 896. 最长上升子序列 II
思路:
本题范围为100000,O(n^2)复杂度会TLE,考虑优化
维护一个单调上升的数组q[],其中q[i]表示最长上升子序列长度为i的所有序列中,结尾最小的一个值;(长度相同的上升子序列中,结尾大的肯定不如结尾小的有前途)
做法:依次遍历a中每一个数,通过二分,查找一个数q[j],是小于a[i]的最大的值,其代表a[i]可以接在长度为j的上升子序列的后面,同时更新最长上升子序列的长度len,即len=max(len,r+1),并对q数组进行更新,即q[j+1]=a[i];
时间复杂度:n次操作,每次二分logn,因此为O(nlogn)
代码:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=100010;
static int a[]=new int[N];
static int q[]=new int[N];
public static void main(String args[]) throws IOException{
n=nextInt();
for(int i=0;i<n;i++) a[i]=nextInt();
int len=0;
q[0]=(int)-2e9;
for(int i=0;i<n;i++){
int l=0,r=len;
while (l<r){//找到最大的小于a[i]的值
int mid=l+r+1>>1;
if(q[mid]<a[i]) l=mid;
else r=mid-1;
}
len= Math.max(len,r+1);//更新len
q[r+1]=a[i];//更新q数组
}
pw.println(len);
pw.close();
}
}
AcWing 897. 最长公共子序列
思路:
以a[i],b[j]是否包含在子序列中划分集合:
00表示子序列中不包含a[i],b[j] 01表示不包含a[i],包含b[j];
10表示子序列中包含a[i],不包含b[j] 11表示包含a[i],b[j] 只有a[i]==b[j]时才有这种情况;
01状态并不等价于f[i-1][j],但f[i-1][j]一定包含01这种状态;属性为max,因此这四种划分方式可以重叠,求最大值时可用f[i-1][j]代替01情况;10情况分析同理;
状态转移方程:f[i][j]=max(f[i-1][j-1],f[i-1][j],f[i][j-1],f[i-1][j-1]+1);
由于f[i-1][j],f[i][j-1]一定包含f[i-1][j-1]的情况,因此求最大值时可以省略f[i-1][j-1]
代码:
import java.io.*;
public class Main {
static BufferedReader bf=new BufferedReader(new InputStreamReader(System.in));
static PrintWriter pw=new PrintWriter(System.out);
static int n,m;
static int N=1010;
static char a[]=new char[N];
static char b[]=new char[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
String s[]=bf.readLine().split(" ");
n=Integer.parseInt(s[0]);
m=Integer.parseInt(s[1]);
a=(" "+bf.readLine()).toCharArray();//使下标从1开始
b=(" "+bf.readLine()).toCharArray();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j]= Math.max(f[i-1][j],f[i][j-1]);
if(a[i]==b[j]) f[i][j]= Math.max(f[i][j],f[i-1][j-1]+1);
}
}
pw.println(f[n][m]);
pw.close();
}
}
AcWing 902. 最短编辑距离
思路:
f[i][j]表示所有从a[1~i]变为b[1~j]的操作方式,属性:Min
按照最后一步操作是什么来划分集合,经过最后一步操作后a[1~i]即可变为b[1~j];
则状态转移方程:f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1,f[i-1][j-1]+0/1));
代码:
import java.io.*;
public class Main {
static BufferedReader bf=new BufferedReader(new InputStreamReader(System.in));
static PrintWriter pw=new PrintWriter(System.out);
static int n,m;
static int N=1010;
static char a[]=new char[N];
static char b[]=new char[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
String s[]=bf.readLine().split(" ");
n= Integer.parseInt(s[0]);
a=(" "+bf.readLine()).toCharArray();
String s1[]=bf.readLine().split(" ");
m=Integer.parseInt(s1[0]);
b=(" "+bf.readLine()).toCharArray();
for(int i=1;i<=m;i++) f[0][i]=i;//只能通过添加操作
for(int i=1;i<=n;i++) f[i][0]=i;//只能通过删除操作
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j]= Math.min(f[i-1][j]+1,f[i][j-1]+1);//删和增操作
if(a[i]==b[j]) f[i][j]= Math.min(f[i][j],f[i-1][j-1]);//若a[i]==b[j],无需+1
else f[i][j]= Math.min(f[i][j],f[i-1][j-1]+1);//若a[i]!=b[j],需要+1
}
}
pw.println(f[n][m]);
pw.close();
}
}
AcWing 899. 编辑距离
思路:
将求最短编辑距离定义为一个方法,参数分别为原字符串,目标字符串,返回最短编辑距离
遍历每个字符串,求其最短编辑距离是否在给定的上限内
代码:
import java.io.*;
public class Main {
static BufferedReader bf=new BufferedReader(new InputStreamReader(System.in));
static PrintWriter pw=new PrintWriter(System.out);
static int n,m;
static int N=1010;
static int M=20;
static String s[]=new String[N];
static int f[][]=new int[M][M];
public static int get_distance(char a[],char b[]){//a到b所需要的最短编辑距离
int la=a.length-1;//a[0],b[0]为空格,长度需-1
int lb=b.length-1;
for(int i=1;i<=lb;i++) f[0][i]=i;//只能通过添加操作
for(int i=1;i<=la;i++) f[i][0]=i;//只能通过删除操作
for(int i=1;i<=la;i++){
for(int j=1;j<=lb;j++){
f[i][j]= Math.min(f[i-1][j]+1,f[i][j-1]+1);
if(a[i]==b[j]) f[i][j]= Math.min(f[i][j],f[i-1][j-1]);
else f[i][j]= Math.min(f[i][j],f[i-1][j-1]+1);
}
}
return f[la][lb];
}
public static void main(String args[]) throws IOException{
String s1[]=bf.readLine().split(" ");
n=Integer.parseInt(s1[0]);
m=Integer.parseInt(s1[1]);
for(int i=0;i<n;i++) s[i]=bf.readLine();
while (m--!=0){
String s2[]=bf.readLine().split(" ");
char c1[]=(" "+s2[0]).toCharArray();//使下标从1开始
int limit=Integer.parseInt(s2[1]);
int res=0;
for(int i=0;i<n;i++){
char c2[]=(" "+s[i]).toCharArray();//使下标从1开始
if(get_distance(c2,c1)<=limit) res++;
}
pw.println(res);
}
pw.close();
}
}
三、区间DP
AcWing 282. 石子合并
思路:
状态表示 集合:f(l,r)表示将(l,r)区间内的所有石子合并成一堆的方案的集合 属性:Min
按照分界点k在什么位置划分集合,以k为分界线,将左半部分合并为一堆,右半部分合并为一堆,最后将两堆合并;
状态转移方程 f(l,r)=min(f(l,k)+f(k+1,r)+s[r]-s[l-1]) k范围:i~j-1
代码:
import java.io.*;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=310;
static int a[]=new int[N];
static int f[][]=new int[N][N];
public static void main(String args[]) throws IOException{
n=nextInt();
for(int i=1;i<=n;i++){
a[i]=nextInt();
a[i]+=a[i-1];
}
for(int len=2;len<=n;len++){//区间dp先枚举区间长度,len=1时自身就是一堆,无需初始化
for(int l=1;l+len-1<=n;l++){//枚举左端点
int r=l+len-1;//获取右端点
f[l][r]=(int)2e9;//初始化为正无穷
for(int k=l;k<r;k++){
f[l][r]= Math.min(f[l][r],f[l][k]+f[k+1][r]+a[r]-a[l-1]);
}
}
}
pw.println(f[1][n]);
pw.close();
}
}
四、计数类DP
AcWing 900. 整数划分
思路:
将该问题转化为一个完全背包问题:
从体积为1,2,3····n共n个物品中选择恰好能装满容量为n的背包中,每个物品的个数没有限制
由于v[i]=i,因此用i代替v[i];
状态表示 集合:f(i,j)表示从前i个物品中选,体积恰好为j的方案 属性:数量
按照第i个物品选几个划分集合,状态转移方程 f(i,j)=Σf(i-1,j-k*i)
代码一 朴素做法:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=1010;
static int f[][]=new int[N][N];
static int mod=(int)1e9+7;
public static void main(String args[]) throws IOException{
n=nextInt();
f[0][0]=1;//表示恰好组成容量为0的方案数,即什么都不选
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++){
for(int k=0;k*i<=j;k++){
f[i][j]=(f[i][j]+f[i-1][j-k*i])%mod;
}
}
}
pw.println(f[n][n]);
pw.close();
}
}
代码二 优化一层循环
将上述状态转移方程优化:f(i,j)=f(i-1,j)+f(i,j-i)
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=1010;
static int f[][]=new int[N][N];
static int mod=(int)1e9+7;
public static void main(String args[]) throws IOException{
n=nextInt();
f[0][0]=1;//表示恰好组成容量为0的方案数,即什么都不选
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++){
f[i][j]=(f[i][j]+f[i-1][j])%mod;
if(j>=i) f[i][j]=(f[i][j]+f[i][j-i])%mod;
}
}
pw.println(f[n][n]);
pw.close();
}
}
代码三 空间优化后:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=1010;
static int f[]=new int[N];
static int mod=(int)1e9+7;
public static void main(String args[]) throws IOException{
n=nextInt();
f[0]=1;//表示恰好组成容量为0的方案数,即什么都不选
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
f[j]=(f[j]+f[j-i])%mod;
}
}
pw.println(f[n]);
pw.close();
}
}
五、数位统计DP
AcWing 338. 计数问题
思路:
思路 实现一个count(n,x) 求1~n中x出现的次数
若想求[a,b]中x出现的次数,则为count(b,x)-count(a-1,x);
如何求1~n中x出现的次数? 分情况讨论(如下图求1在第4位出现的次数为例)
按照这种方式求x在每一位出现的次数,最后进行累加即可得到x在1~n中出现的次数
注意:由于不能存在前导0,即0001234会写成1234,当求0出现的次数时,第一种情况xxx=001~abc共(abc-1)*1000种情况;
①当求0出现次数即x=0时,对应的第一种情况的方案数为(abc-1)*1000
②当求0出现次数即x=0时,由高位向低位遍历时从n-2开始(n-1为首位,0一定不在首位)
代码:
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
public static int power10(int x){//返回10^x
int res=1;
while (x!=0){
res*=10;
x--;
}
return res;
}
public static int get_num(List<Integer>list,int l,int r){//返回list中第l到r位组成的数字
int res=0;
for(int i=l;i>=r;i--) res=res*10+list.get(i);
return res;
}
public static int count(int n,int x){//统计1~n中x出现的次数
if(n==0) return 0;
List<Integer>list=new ArrayList<>();//得到n的每一位
while (n!=0){
list.add(n%10);
n/=10;
}
n=list.size();//n的位数
int res=0;
for(int i=n-1-(x==0?1:0);i>=0;i--){//从高位向低位枚举,求每一位中x出现的次数
if(i<n-1){//i不是最高位
res+=get_num(list,n-1,i+1)*power10(i);
if(x==0) res-=power10(i);//x=0时特殊处理
}
if(list.get(i)==x) res+=get_num(list,i-1,0)+1;
else if(list.get(i)>x) res+=power10(i);
}
return res;
}
public static void main(String args[]) throws IOException{
while (true){
int a=nextInt();
int b=nextInt();
if(a==0 && b==0) break;
if(a>b){//保证a小于等于b
int temp=b;
b=a;
a=temp;
}
for(int i=0;i<10;i++){
pw.print(count(b,i)-count(a-1,i)+" ");
}
pw.println();
}
pw.close();
}
}
六、状态压缩DP
AcWing 291. 蒙德里安的梦想
思路:
状态压缩DP:一般是用二进制数表示一种状态
当横向方块摆好时,纵向方块只有一种方法,因此求横向方块的合法摆放方案的数量即可
状态表示 集合:f[i][j]表示要摆放第i列,其中第i-1列的状态为j,上一列中哪一行伸出来则为1,否则为0 属性:方案数;
判断是否合法:设第i-2伸到第i-1列的状态为k,则需满足伸出的不能是同一行即 (j&k)==0 ,且所有连续空着的0的长度必须是偶数个,表示可以放下纵向方块
最终答案即是f[m][0] 表示从第m-1(最后一列)伸出的状态是0(即没有伸出)也就是填满了表格的方案数
时间复杂度: 11*2^11*2^11≈4*10^7;
代码:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=12,M=1<<N;
static long f[][]=new long[N][M];
static int state[][]=new int[M][M];//state[i][j]=1表示i状态和j状态既不冲突且连续空着的长度是偶数
static boolean flag[]=new boolean[M];//判断当前列能否用纵向方块填满,即判断连续空着的长度是否是偶数
public static void main(String args[]) throws IOException{
while (true){
n=nextInt();
m=nextInt();
if(n==0 && m==0) break;
for(int i=0;i<1<<n;i++){//枚举列的所有状态
int cnt=0;//表示连续0的个数
boolean is_valid=true;
for(int j=0;j<n;j++){//每种状态有n位
if((i>>j&1)==1){//如果第j位是1,则进行判断
if((cnt&1)==1){//cnt为奇数
is_valid=false;
break;
}
cnt=0;//重新计数
}
else cnt++;
}
if((cnt&1)==1) is_valid=false;//判断最后一段连续0的个数是否为奇数
flag[i]=is_valid;
}
for(int i=0;i<1<<n;i++){
Arrays.fill(state[i],0);
for(int j=0;j<1<<n;j++){
if((i&j)==0 && flag[i|j]) state[i][j]=1;//i与j不冲突,且该状态具有连续偶数个0
}
}
for(int i=0;i<N;i++) Arrays.fill(f[i],0);//清空数组
f[0][0]=1;//什么都不放时的方案
for(int i=1;i<=m;i++){//枚举所有列
for(int j=0;j<1<<n;j++){//枚举第i列的状态
for(int k=0;k<1<<n;k++){//枚举第i-1列的状态
if(state[j][k]==1) f[i][j]+=f[i-1][k];
}
}
}
pw.println(f[m][0]);
}
pw.close();
}
}
AcWing 91. 最短Hamilton路径
思路:
状态表示 集合:f(i,j)表示从0走到j,经过的点是i的所有路径, 属性:Min
以倒数第二个点是什么划分集合,k代表从0走到k,最后一步是k->j
状态转移方程: f(i,j)=f(i-{j},k)+a(k,j) 其中k=0~n-1;
初始状态f[1][0]=1,表示只走了0这个点,最终答案:f[(1<<n)-1][n-1]表示从0~n-1每个点都走了一遍,且最后到达n-1;
代码:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=20;
static int M=1<<20;
static int INF=0x3f3f3f3f;
static int a[][]=new int[N][N];
static int f[][]=new int[M][N];
public static void main(String args[]) throws IOException{
n=nextInt();
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
a[i][j]=nextInt();
}
}
for(int i=0;i<1<<N;i++) Arrays.fill(f[i],INF);//初始化为正无穷
f[1][0]=0;//从0走到0的距离
for(int i=0;i<1<<n;i++){
for(int j=0;j<n;j++){
if((i>>j&1)==1){//i状态包含j这个点
for(int k=0;k<n;k++){
if((i-(1<<j)>>k&1)==1){//i-{j}状态包含k这个点
f[i][j]= Math.min(f[i][j],f[i-(1<<j)][k]+a[k][j]);
}
}
}
}
}
pw.println(f[(1<<n)-1][n-1]);
pw.close();
}
}
七、树形DP
AcWing 285. 没有上司的舞会
思路:
状态有两种:f[u][1]和f[u][0]
f[u][1]表示以u为根的树,且选择u这个点的方案
f[u][0]表示以u为根的树,且不选择u这个点的方案 属性:Max
求f[u][0],则其每一个子树si可选可不选,方案数分别为f[si][0],f[si][1],选择其中的较大者进行累加
f[u][1]表示选择了u,则其子树si不能选,因此直接加上f[si][0];
状态转移方程: f(u,0)=Σmax(f(si,0),f(si,1)) f(u,1)=Σf(si,0); si是u的所有孩子
代码:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n;
static int N=6010;
static int happy[]=new int[N];
static int h[]=new int[N];
static int e[]=new int[N];
static int ne[]=new int[N];
static int idx=0;
static int f[][]=new int[N][2];
static boolean flag[]=new boolean[N];//判断每个点是否有父节点
public static void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
public static void dfs(int u){//求出f[u][0]及f[u][1]
f[u][1]=happy[u];
for(int i=h[u];i!=-1;i=ne[i]){//遍历每个下属
int j=e[i];
dfs(j);
f[u][0]+= Math.max(f[j][0],f[j][1]);
f[u][1]+=f[j][0];
}
}
public static void main(String args[]) throws IOException{
n=nextInt();
for(int i=1;i<=n;i++) happy[i]=nextInt();
Arrays.fill(h,-1);
for(int i=0;i<n-1;i++){
int a=nextInt();
int b=nextInt();
flag[a]=true;//a有父节点,父节点是b
add(b,a);
}
int root=1;
while (flag[root]) root++;//寻找根节点
dfs(root);
int res= Math.max(f[root][0],f[root][1]);
pw.println(res);
pw.close();
}
}
八、记忆化搜索
AcWing 901. 滑雪
思路:
状态表示 集合:f(i,j)表示所有从(i,j)开始滑的路径 属性:Max
按照第一步往哪滑划分集合,可向上左下右滑, 注意这四种情况不一定全部都存在,只有不越界且滑的下一个位置低于原位置时才存在
记忆化搜索,开辟一个f数组,初始化为-1
代码:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static PrintWriter pw=new PrintWriter(System.out);
public static int nextInt() throws IOException{
st.nextToken();
return (int)st.nval;
}
static int n,m;
static int N=310;
static int h[][]=new int[N][N];
static int f[][]=new int[N][N];//f[i][j]存储(i,j)位置能滑的最远距离
static int dx[]={0,0,1,-1};
static int dy[]={1,-1,0,0};
public static int dfs(int x,int y){//求从(x,y)位置能滑的最远距离
if(f[x][y]!=-1) return f[x][y];//已经求过,直接返回
f[x][y]=1;//最少滑自己1个格子
for(int i=0;i<4;i++){
int x1=x+dx[i];
int y1=y+dy[i];
if(x1>=1 && x1<=n && y1>=1 && y1<=m && h[x][y]>h[x1][y1]){//不越界且滑的下一个位置低于原位置
f[x][y]= Math.max(f[x][y],dfs(x1,y1)+1);
}
}
return f[x][y];
}
public static void main(String arsg[]) throws IOException{
n=nextInt();
m=nextInt();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
h[i][j]=nextInt();
}
}
for(int i=1;i<=n;i++) Arrays.fill(f[i],-1);//初始化为-1,表示还没求过从该点出发的最远距离
int res=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
res= Math.max(res,dfs(i,j));
}
}
pw.println(res);
pw.close();
}
}