图片说明
当属性是最大值或者最小值的时候,划分子问题可以有重复的部分,但是如果求元素的数量,那么划分子问题不能有重复的部分

常用模型:背包

优化的时候,如果用上一层体积,就从大到小枚举,如果用本层体积,就从小到大枚举

01背包 --每件物品最多只用一次

图片说明
图片说明
图片说明

const N int = 1010
func knapsack01(N []int,m int) int{
    n := len(N)
    //dp[i][j]表示选取前i件物品且背包容量为j时,背包中物品的最大价值
    var dp [][]int = make([][]int,n+1)
    for i := 0 ; i <= n ; i++ {
        dp[i] = make([]int,m+1)    
    }

    for i := 1 ; i < n ; i++ {
        for j := 0 ; j <= m ; j++ {
            dp[i][j] = dp[i-1][j]
            if(j >= v[i]){
                dp[i][j] = max(dp[i][j],dp[i-1][j-v[i]]+w[i])
            }
        }
    }
    return dp[n][m]
}

将其优化成一维

func knapsack01(N []int,m int) int{
    n := len(N)
    var dp []int = make([]int,m+1)

    for i := 1 ; i < n ; i++ {
        for j := m ; j >= v[i] ; j-- {
            dp[j] = max(dp[j],dp[j-v[i]]+w[i])
        }
    }
    return dp[m]
}

完全背包--每件物品使用次数没有限制(枚举第i个物品选几个)

图片说明

func completeBackpack(N []int,m int) int {
    n := len(N)
    var dp [][]int = make([][]int ,n+1)
    for i := 0 ; i <= n ; i++ {
        dp[i] = make([]int,m+1)
    }
    for i := 1 ; i <= n ; i++ {
        for j := 0 ; j <= m ; j++ {
            for k := 0 ; k * v[i] <= j ; k++ {
                dp[i][j] = max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i])
            }
        }
    }
    return dp[n][m]
}

优化后的完全背包问题-优化成二维
图片说明

func completeBackpack(N []int,m int) int {
    n := len(N)
    var dp [][]int = make([][]int ,n+1)
    for i := 0 ; i <= n ; i++ {
        dp[i] = make([]int,m+1)
    }
    for i := 1 ; i <= n ; i++ {
        for j := 0 ; j <= m ; j++ {
            dp[i][j] = dp[i-1][j]
            if j >= v[i] {
                dp[i][j] = max(dp[i][j],dp[i][j-v[i]]+w[i])
            }

        }
    }
    return dp[n][m]
}

优化成一维

func completeBackpack(N []int,m int) int {
    n := len(N)
    var dp []int = make([]int ,m+1)

    for i := 1 ; i <= n ; i++ {
        for j := v[i] ; j <= m ; j++ {
            dp[j] = max(dp[j],dp[j-v[i]]+w[i])  
        }
    }
    return dp[m]
}

多重背包问题 --每种物品最多有Si个

图片说明

func completeBackpack(N []int,m int,s []int) int {
    n := len(N)
    var dp [][]int = make([][]int ,n+1)
    for i := 0 ; i <= n ; i++ {
        dp[i] = make([]int,m+1)
    }
    for i := 1 ; i <= n ; i++ {
        for j := 0 ; j <= m ; j++ {
            for k := 0 ; k <= s[i] && k * v[i] <= j ; k++ {
                dp[i][j] = max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i])
            }
        }
    }
    return dp[n][m]
}

使用二进制的方式进行优化

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N];
int f[M];

int main()
{
    cin >> n >> m;

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;//从1开始分,只要k<=s,每次把k个第i个物品打包在一起
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0) //把最后剩下的s个物品组到一块
        {
            cnt ++ ;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;

    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;

    return 0;
}

分组背包问题 --物品有N组,每一组物品有若干个,每组里面最多只能选一个物品(枚举第i组物品选哪个)

图片说明

func groupBackpack(v [][]int,w [][]int,s []int ,m int){
    n := len(v)
    var dp []int = make([]int,n+1)
    for i := 0 ; i <= n ; i++{
        dp[i] = make([]int,m+1)
    }
    for i := 1 ; i <= n ; i++ {
        for j := 0 ; j <= 0 ; j++ {
            for k := 0 ; k < s[i] ; k ++{
                dp[i][j] = max(dp[i][j],dp[i-1][j-v[i][k]]+w[i][k])
            }
        }
    }
    return dp[n][m]
}
func groupBackpack(v [][]int,w [][]int,s []int ,m int){
    n := len(v)
    var dp []int = make([]int,m+1)
    for i := 1 ; i <= n ; i++ {
        for j := m ; j >= 0 ; j-- {
            for k := 0 ; k < s[i] ; k ++{
                dp[j] = max(dp[j],dp[j-v[i][k]]+w[i][k])
            }
        }
    }
    return dp[m]
}

不同类型DP

线性DP

图片说明
图片说明

func maximumTotal(triangle [][]int) int {
    if len(triangle) == 0 || len(triangle[0]) == 0 {
        return 0
    }
    n := len(triangle)
    var dp [][]int = make([][]int,n)
    for i := 0 ; i < n ; i ++ {
        dp[i] = make([]int,len(triangle[i]))
    }
    dp[0][0] = triangle[0][0]
    for i := 1 ; i < n ; i ++ {
        for j := 0 ; j <= i;j++ {
            if j == 0 {
                dp[i][j] = dp[i-1][j]+triangle[i][j]
            }else if j == i {
                dp[i][j] = dp[i-1][j-1]+triangle[i][j]
            }else {
                dp[i][j] = max(dp[i-1][j]+triangle[i][j],dp[i-1][j-1]+triangle[i][j])
            }
        }
    }
    sort.Ints(dp[n-1])
    m := len(dp[n-1])
    return dp[n-1][m-1]
}
func max(a,b int) int{
    if a > b {
        return a
    }
    return b
}

优化之后

func maximumTotal(triangle [][]int) int {
    if len(triangle) == 0 || len(triangle[0]) == 0 {
        return 0
    }
    n := len(triangle)
    var dp []int = make([]int,n)

    dp[0] = triangle[0][0]
    for i := 1 ; i < n ; i ++ {
        for j := i ; j >= 0;j-- {
            if j == 0 {
                dp[j] = dp[j]+triangle[i][j]
            }else if j == i {
                dp[j] = dp[j-1]+triangle[i][j]
            }else {
                dp[j] = max(dp[j]+triangle[i][j],dp[j-1]+triangle[i][j])
            }
        }
    }
    sort.Ints(dp)
    return dp[n-1]
}
func max(a,b int) int{
    if a > b {
        return a
    }
    return b
}

图片说明
图片说明

//dp[i]表示所有以i结尾的上升子序列的最大长度
func lengthOfLIS(nums []int) int {
    n := len(nums)
    if n == 0 {
        return 0
    }
    var dp []int = make([]int,n)
    for i := 0 ; i < n ; i++{
        dp[i] = 1
    }
    for i := 0 ; i < n ; i++ {
        for j := 0 ; j < i ; j++{
            if nums[i] > nums[j]{
                dp[i] = max(dp[i],dp[j]+1)
            }
        }
    }
    sort.Ints(dp)
    return dp[n-1]

}
func max(a,b int)int {
    if a > b {
        return a
    }
    return b
}

用来输出最长上升子序列
图片说明

时间复杂度的优化O(nlongn)

图片说明
图片说明

func longestCommonSubsequence(text1 string, text2 string) int {
    n := len(text1)
    m := len(text2)
    var dp [][]int = make([][]int,n+1)
    for i := 0 ; i <= n ; i++ {
        dp[i] = make([]int,m+1)
    }
    text1 = " " + text1
    text2 = " " + text2
    for i := 1 ; i < n + 1 ; i++ {
        for j := 1 ; j < m + 1 ; j++ {
            dp[i][j] = max(dp[i-1][j],dp[i][j-1])
            if text1[i] == text2[j] {
                dp[i][j] = max(dp[i][j],dp[i-1][j-1]+1)
            }
        }
    }
    return dp[n][m]
}
func max(a,b int) int {
    if a > b {
        return a
    }
    return b
}

图片说明

//dp[i][j]表示从word1[1..i]到word2[1..j]需要的最少操作数
func minDistance(word1 string, word2 string) int {
   n := len(word1)
   m := len(word2)
   word1 = " " + word1
   word2 = " " + word2
   dp := make([][]int,n+1)
   for i := 0 ; i <= n ; i++ {
       dp[i] = make([]int,m+1)
   }
   for i := 0 ; i <= n ; i++ {
       dp[i][0] = i
   }
   for i := 0 ;  i <= m ; i++ {
       dp[0][i] = i
   }
   for i := 1 ; i <= n ; i++{
       for j := 1 ; j <= m ; j++ {
           dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1)
           t := 0
           if word1[i] != word2[j]{
               t = 1
           }
           dp[i][j] = min(dp[i][j],dp[i-1][j-1]+t)
       }
   }
   return dp[n][m]
}
func min(a,b int)int{
    if a < b {
        return a
    }
    return b
}

区间DP问题

有 N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。

每次移动(move)需要将连续的 2 堆石头合并为一堆,而这个移动的成本为这 2 堆石头的总数。

找出把所有石头合并成一堆的最低成本。

图片说明

func miniMumCost(stone []int) int {
    n := len(stone)
    //用来求前缀和
    var s []int = make([]int,n)
    s[0] = stone[0]
    for i := 1 ; i < n ; i++ {
        s[i]=s[i-1]+a[i]
    }
    var dp [][]int = make([][]int,n)
    for i := 0 ; i < n ; i++ {
        dp[i] = make([]int,n)
    }
    for i := 1 ; i < n ; i++ {
        for j := 0 ; j + i -1 < n ; j++{
            l,r:=j,j+i-1
            dp[l][r] = 1e8
            for k := l ; k < r ; k++ {
                dp[l][r] = min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]-s[l-1])
            }
        }
    }
    return dp[0][n-1] 
}

数位统计DP

计数问题
图片说明
图片说明

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector<int> num, int l, int r)
{
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x)
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;

    vector<int> num;
    while (n)
    {
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

    int res = 0;
    for (int i = n - 1 - !x; i >= 0; i -- )
    {
        if (i < n - 1)
        {
            res += get(num, n - 1, i + 1) * power10(i);
            if (!x) res -= power10(i);
        }

        if (num[i] == x) res += get(num, i - 1, 0) + 1;
        else if (num[i] > x) res += power10(i);
    }

    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);

        for (int i = 0; i <= 9; i ++ )
            cout << count(b, i) - count(a - 1, i) << ' ';
        cout << endl;
    }

    return 0;
}

状态压缩DP

蒙德里安的梦想
图片说明
代码主要分为两个部分(横着摆放的方式确定后,竖着摆放的方式就已确定):

枚举每一列合法的占位方式,记录在str数组内
(把连续空格数为奇数的状态设定为false,因为空格要摆放竖着的方块,单个空格无法摆放方块)

开始DP

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N;//M的每一位二进制位储存一种状态

int n, m;
long long f[N][M];
bool st[M];//储存每一列上合法的摆放状态

//f[i][j]表示摆放第i列,i列向后伸出来横着的方格状态为j的方案数,j为一个二进制数,用01表示是否戳出来
int main()
{
    while (cin >> n >> m, n || m)
    {
        //枚举每一列的占位状态里哪些是合法的
        for (int i = 0; i < 1 << n; i ++ )//一共n行,枚举n位不同的状态
        {
            int cnt = 0;//用来记录连续的0的个数
            st[i] = true;//记录这个状态被枚举过且可行
            for (int j = 0; j < n; j ++ )//从低位到高位枚举它的每一位
                if (i >> j & 1)//如果为1
                {
                    if (cnt & 1) st[i] = false;//如果之前连续0的个数是奇数,竖的方块插不进来,这种状态不行
                    cnt = 0;//清空计数器
                }
                else cnt ++ ;//如果不为1,计数器+1
            if (cnt & 1) st[i] = false;//到末尾再判断一下前面0的个数是否为奇数,同前
        }

        memset(f, 0, sizeof f);//一定要记得初始化成0,对于每个新的输入要重新计算f[N][M]
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )//对于每一列
            for (int j = 0; j < 1 << n; j ++ )//枚举j的状态
                for (int k = 0; k < 1 << n; k ++ )//再枚举前一行的伸出状态k
                    if ((j & k) == 0 && st[j | k])//如果它们没有冲突,i这一列被占位的情况也是合法的话
                        f[i][j] += f[i - 1][k];//那么这种状态下它的方案数等于之前每种k状态数目的和

        cout << f[m][0] << endl;//求的是第m行排满,并且第m行不向外伸出块的情况
    }
    return 0;
}

最短Hamilton路径
题目描述
给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。

输入格式
第一行输入整数n。

接下来n行每行n个整数,其中第i行第j个整数表示点i到j的距离(记为a[i,j])。

对于任意的x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]>=a[x,z]。

输出格式
输出一个整数,表示最短Hamilton路径的长度。

数据范围
1≤n≤201≤n≤20
0≤a[i,j]≤1070≤a[i,j]≤107
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18

图片说明

#include <bits/stdc++.h>
using namespace std;
int n,f[1<<20][21],i,j,k;
int weight[21][21];
int main()
{
    ios::sync_with_stdio(false);//加快cin的读入速度,但是scanf将会不能用。
    memset(f,0x3f,sizeof(f));//初始化最大值
    cin>>n;
    for (i=0; i<n; i++)
        for (j=0; j<n; j++)
            cin>>weight[i][j];
    f[1][0]=0;//第一个点是不需要任何费用的
    for (i=1; i<(1<<n); i++)//i代表着是一个方案集合,其中每一个位置1和0,代表着这个点经过还是没有经过
        for (j=0; j<n; j++)//枚举当前到了哪一个点
            if ((i>>j & 1))//如果i集合中第j位是1,也就是到达过这个点
                for (k=0; k<n; k++)//枚举到达j的点k
                    if ((i^(1<<j)) >> k & 1)//重点,判断k和j的条件,具体在上面解说
                        f[i][j]=min(f[i][j],f[i^(1<<j)][k]+weight[k][j]);//选择最小值,也就是判断,k点到j点最优,还是以前的方案最优
    cout<<f[(1<<n)-1][n-1];//输出最后的最优值
    return 0;
}

树形DP

没有上司的舞会
Ural大学有N名职员,编号为1~N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式
第一行一个整数N。

接下来N行,第 i 行表示 i 号职员的快乐指数Hi。

接下来N-1行,每行输入一对整数L, K,表示K是L的直接上司。

最后一行输入0,0。

输出格式
输出最大的快乐指数。

数据范围
1≤N≤6000,
−128≤Hi≤127

样例
输入样例:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
0 0

图片说明

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;//因为树可以看做特殊的图,所以用邻接表来存储树
int happy[N];//每个人的高兴度
int f[N][2];//所有状态 
bool has_fa[N];//判断每个点是否有父节点

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][1] = happy[u];//如果选择这个点就需要加上其高兴度

    for (int i = h[u]; ~i; i = ne[i])//枚举u的所有儿子
    {
        int j = e[i];//u的某个儿子
        dfs(j);

        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

int main()
{
    scanf("%d", &n);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);

    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(b, a);//在邻接表中插入这条边
        has_fa[a] = true;
    }

    int root = 1;
    while (has_fa[root]) root ++ ;

    dfs(root);

    printf("%d\n", max(f[root][0], f[root][1]));

    return 0;
}

记忆化搜索

滑雪
图片说明
图片说明

func slide(h [][]int) int{
    n := len(h)
    m := len(h[0])
    var f [][]int = make([][]int,n)
    for i := 0 ; i < n ; i++ {
        f[i] = make([]int,m)
    }
    dx := []int{-1,0,1,0}
    dy := []int{0,1,0,-1}
    var dp func(int,int) int
    dp = func(x int,y int)int{
        if f[x][y] != 0 {return f[x][y]}
        f[x][y] := 1
        for i := 0 ; i < 4 ; i++ {
            a := x + dx[i]
            b := y + dy[i]
            if a >= 0 && a < n && b >=0 && b < m && h[a][b] < h[x][y] {
                f[x][y] = max(f[x][y],dp(a,b)+1)
            }
        }
        return f[x][y]
    }
   res := 0
    for i := 0 ; i < n ; i++ {
        for j := 0 ; j < m ; j++ {
            res = max(res,dp(i,j))
        }
    }
    return res
}