AcWing算法基础课复习——(五)动态规划

一、背包问题

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();
    }
}

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值