Acwing动态规划算法

动态规划


闫式dp.jpg

背包问题

01背包

  • 朴素算法

  • #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    using namespace std;
    const int M = 1010;
    int v[M], w[M];
    int f[M][M]; // f[i][j] 前i个数中选择若干个物品,并且其体积之和小于等于j的最大价值
    int main()
    {
        int n , m;
        cin >> n >> m;
        for (int i = 1; i <= n; i ++ ) // 下标从1开始
        {
            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]; // 不选择第i个物品
                if(j >= v[i]) // 当j >= v[i] 时,才能选择第i个物品
                {
                    f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 不选择第i个物品和,选择第i个物品做对比
                }
            }
        }
        cout << f[n][m] << endl;
        return 0;
    }
    
  • 优化后代码

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

using namespace std;
const int M = 1010;
int v[M], w[M];
// int f[M][M]; // f[i][j] 前i个数中选择若干个物品,并且其体积之和小于等于j的最大价值
int f[M];   // 优化后将f[i][j]变为一维数组f[j], 其中j为体积

int main()
{
    int n , m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) // 下标从1开始
    {
        cin >> v[i] >> w[i];
    }
    
    for (int i = 1; i <= n; i ++ )
    {
        for(int j = m; j >= 0; j--)
        {
            // f[i][j] = f[i - 1][j]; // 不选择第i个物品
            // 优化后 f[j] = f[j];
            if(j >= v[i]) // 当j >= v[i] 时,才能选择第i个物品
            {
                // f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 不选择第i个物品和,选择第i个物品做对比
                f[j] = max(f[j], f[j - v[i]] + w[i]); // 将j从大到小遍历,算出的f[j - v[i]], 即为i-1得时候的f[j - v[i]],
                                                      //因为此时f[j - v[i]]尚未被赋值
            }
        }
        // 发现f[i][j]的第一维,可以优化掉
    }
    cout << f[m] << endl;
    return 0;
}

3.完全背包

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
// 完全背包问题和0,1背包问题的区别,完全背包问题的选择问题可以分为不选、选1个、选2个、选3个.....选n个等
int f[N][N]; // f[i][j] 为在i个种类中,选择容量小于等于j的最大价值
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(j >= v[i])
                f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]); // 与01背包不同点就在于01背包为f[i-1][j-vi] + w[i], 而完全背包为f[i][j - v[i]] + w[i];
    
    // 优化后j的遍历顺序从小到大,不需要和01背包一样从大到小遍历。。。
    cout << f[n][m];
    return 0;
}

4 多重背包问题

  • 朴素算法
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 110;
int f[N][N];
//f[i][j]含义同01背包和完全背包问题,即选择前i个物品,体积不大于j的最大价值
int v[N], w[N], s[N];
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> v[i] >> w[i] >> s[i];   
    }   
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
            for(int k = 0; k <= s[i]; k++)
            {
                // f[i][j] = f[i-1][j];
                // 不能写f[i][j] = f[i-1][j],
                //因为多重背包问题考虑了不选择第i个物品的情况后,如果j>=k*v[i],并且选择了选若干个第i个物品的情况;
                //第二次在进行f[i][j] = f[i-1][j]会将最大值给覆盖掉
                if(j >= k * v[i])
                    f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
    cout << f[n][m];
    return 0;
}
  • 优化后的算法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2021;
const int M = 12010;
// 将每个物品的数量s优化为 1, 2, 4, 8, ...等多个包,每个都可以选择或者不选择,排列组合起来能够得到s的值
//log2000  < 11,取12,与N相乘取M==12010;
int f[N];
int v[M], w[M];
int main()
{
    int n, m;
    cin >> n >> m;
    int cnt = 0; //此时cnt相当于包含了i*s的所有数据的所以,相当于01背包的n
    for (int i = 1; i <= n; i ++ )
    {    
        int a, b, s;
        cin >> a >> b >> s;
        for(int k = 1; k <= s; k *= 2) // 优化操作
        {
            cnt++;
            s -= k;
            v[cnt] = k * a;
            w[cnt] = k * b;
        }
        if(s)
        {
            cnt++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }    
    n = cnt; // 将cnt的值传递给n,确保能够计算到所有情况
    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];
    return 0;
}

9. 分组背包

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

using namespace std;

const int N = 110;
int v[N][N], w[N][N];
int s[N]; // s[i] 为第i组的物品数量
int f[N]; // f[i][j] ---> f[j],省去第一维,f[i][j]代表前i组,体积不大于j的最大价值
// 分组背包同多重背包不同:多重背包是选择第i个物品选k个,而分组背包是选择第i组物品中的第k个
int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> s[i];
        for(int j = 1; j <= s[i]; j++)
            cin >> v[i][j] >> w[i][j];
    }
    for(int i = 1; i <= n; i++)
        for(int j = m; j >= 0; j--)
            for(int k = 1; k <= s[i]; k++)
                if(j >= v[i][k])
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m];
    return 0;
}

线性DP

898. 数字三角形

  • 像求路径这种线性问题,一般都是将状态表示表示为路径坐标,如f[i][j],同时要考虑边界问题。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int f[N][N], w[N][N]; //f[i][j] 表示从底层到(i,j)的路径最大值,w[i][j],表示(i,j)的值
//本题从底层向上计算路径最大值,避免一些边界问题
int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            cin >> w[i][j];

    for(int i = 1; i <= n; i++) f[n][i] = w[n][i]; // 将最后一层的数赋值给f[][];
    for(int i = n; i; i --)
        for(int j = 1; j <= i; j ++)
            f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + w[i][j];
            
    cout << f[1][1]; 
    return 0;
}

895. 最长上升序列

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N];//状态表示
int p[N];//输入整数
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> p[i];
    f[1] = 1;
    for(int i = 1; i <= n; i++) // 
    {
        f[i] = 1;
        for(int j = 1; j <= i; j++)
        {
            if(p[i] > p[j]) //如果p[i] > p[j],证明存在比f[j]更大的单调递增子序列,但是不一定f[j]+1和f[i]的值谁大谁小
                f[i] = max(f[i], f[j] + 1);
        }
    }
    int res = 0;
    for(int i = 1; i <= n; i++) res = max(res, f[i]);
    cout << res;
    return 0;
}

896. 最长上升序列

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int q[N]; // q数组存入输入序列
int a[N]; // a[i] = j, 表示最大上升子序列长度为i的最后一个数为j
int main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n; i++)
        cin >> q[i];
    
    int cnt = 0; // 表示最大上升子序列的长度
    for(int i = 0; i < n; i++) // 在a[cnt]数组中寻找此时q[i]所在的位置,如果比所有的a[i]都大,证明cnt需要增加,要是比某个数小,则需要将q[i]替换a[]
    {
        int l = 0, r = cnt;
        while(l < r)
        {
            int mid = (l + r + 1) / 2;
            if(q[i] > a[mid]) l = mid;
            else r = mid - 1;
        }
        cnt = max(cnt, r + 1); // 如果 r+1大于cnt,证明r+1比a的长度长--q[i]大于a[cnt],所以将cnt更新
        a[r + 1] = q[i];//将表示最大上升子序列长度为r+1的最后一个数更新为q[i]
    }
    cout << cnt;
    
    return 0;
}

897.最长公共子序列

问题分析.PNG

如果两个字符相等,就可以直接转移到f[i-1][j-1],不相等的话,两个字符一定有一个可以抛弃,可以对f[i-1][j],f[i][j-1]两种状态取max来转移

状态转移.PNG

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N]; // f[i][j]表示长度为i和j的字符串A和B的最大公共子序列
char a[N], b[N];// a,b为两个数组
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++)
        {
            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);
        }
    }
    cout << f[n][m];
    return 0;
}

902.最短编辑距离

状态表示 dp[i][j]
    集合 : 所有吧a中的前i个字母 变成 b中前j个字母的集合的操作集合
    属性 : 所有操作中操作次数最少的方案的操作数
状态计算
状态划分 以对a中的第i个字母操作不同划分
    在该字母之后添加
        添加一个字母之后变得相同,说明没有添加前a的前i个已经和b的前j-1个已经相同
        即 : dp[i][j] = dp[i][j-1] + 1
    删除该字母
        删除该字母之后变得相同,说明没有删除前a中前i-1已经和b的前j个已经相同
        即 : dp[i][j] = dp[i-1][j] + 1
    替换该字母
        替换说明对应结尾字母不同,则看倒数第二个
        即: dp[i][j] = dp[i-1][j-1] + 1
    啥也不做
        对应结尾字母相同,直接比较倒数第二个
        即: dp[i][j] = dp[i-1][j-1]
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N]; // f[i][j]为状态表示,表示第一个数组前i个数和第二个数组前j个数的最少操作次数
int main()
{
    cin >> n >> a + 1 >> m >> b + 1;
    
    for (int i = 1; i <= n; i ++ ) f[i][0] = i; // 当第二个数组为空的时候,删除第一个数组需要进行的步骤
    for (int i = 1; i <= m; i ++ ) f[0][i] = i; // 当第一个数组为空的时候,增加第一个数组需要进行的步骤
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            f[i][j] = min(f[i-1][j], f[i][j-1]) + 1; // f[i-1][j] 为需要删除a[i]的状态,f[i][j-1] 为需要增加a[i]的状态
            
            f[i][j] = min(f[i][j], f[i-1][j-1] + (a[i] != b[j])); // f[i-1][j-1] + 1为a[i] != b[j],需要修改a[i]的情况; f[i-1][j-1]为a[i]=b[j]的情况
        }
    }
    cout << f[n][m];
    return 0;
}

899. 编辑距离

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
const int M = 11;
char a[N][M]; // 输入的字符串
int distance(char a[], char b[])
{
    int f[N][M]; // 状态表示
    int la = strlen(a + 1);
    int lb = strlen(b + 1);
    
    for(int i = 1; i <= la; i++) f[i][0] = i; // 删除操作
    for(int i = 1; i <= lb; i++) f[0][i] = i; // 增加操作
    
    for(int i = 1; i <= la; i++)
        for(int j = 1; j <= lb; j++)
        {
            f[i][j] = min(f[i-1][j], f[i][j-1]) + 1; // f[i-1][j]是删除a[i],f[i][j-1]是增加a[i];
            f[i][j] = min(f[i][j], f[i-1][j-1] + (a[i] != b[j])); // 如果a[i] = b[j], f[i-1][j-1] = f[i][j], 如果不等于就加一
        }
        return f[la][lb];
}

int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i] + 1;
    
    while(m--)
    {
        int q; // 操作限制
        char b[M]; // 查询字符串
        cin >> b + 1 >> q;
        
        int res = 0; // 达到要求的字符串数量
        for(int i = 1; i <= n; i++)
        {
            int d = distance(a[i], b);
            if(d <= q) res++;
        }
        cout << res << endl;
    }   
    return 0;
}

区间DP

282.石子合并

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int s[N]; // 前缀和
int f[N][N]; // f[i][j]表示从索引从i到j石子合并所需的最小代价
int a[N]; // 每一堆石子代价
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        s[i] = s[i-1] + a[i];
    }
    
    for(int len = 2; len <= n; len++) // 石子长度:最外围
    {
        for(int i = 1; i + len - 1 <= n; i++)
        {
            int l = i, r = i + len - 1; //l为开始索引,r为结束索引
            f[l][r] = 1e9;//设置一个比较大的值
            for(int j = l; j <= r; j++)
            {
                //将石子分为两堆,发现这两堆的计算最小代价没有关联。
                //所以就将f[l][r]与这两堆的最小代价+s[r] - s[l-1]相比较
                f[l][r] = min(f[l][r], f[l][j] + f[j + 1][r] + s[r] - s[l-1]);
            }
        }
    }
    cout << f[1][n];
    return 0;
}

计数类DP

900. 整数划分

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

using namespace std;
const int mod = 1e9 + 7;
// 本题可以看成一个完全背包问题
const int N = 1010;
int f[N][N]; // 状态表示:f[i][j]表示从整数1-i中选择的对整数j的划分的数量

int main()
{
    int n;
    cin >> n;
    
    for(int i = 0; i <= n; i++) f[i][0] = 1;
    
    for(int i =1; i<= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            // f[i-1][j]表示不选择整数i,组合成j的总划分数量
            // f[i][j-i]表示选择整数i,但是
            f[i][j] = f[i-1][j] % mod;
            if(j >= i)
                f[i][j] = (f[i-1][j] + f[i][j-i]) % mod;
        }
    }
    cout << f[n][n];
    return 0;
}

状态压缩DP

91. 最短Hamilton路径

  • 时间复杂度2n*n2

  • #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    const int N = 20;
    const int M = 1 << 20; // M 表示一共有M条路径,路径使用二进制来表。0表示没有经过,1表示经过
    int f[M][N]; //状态表示f[i][j]表示从0到j,一共有i条Hamilton路径,最短hamilton路径
    int w[N][N];
    int main()
    {
        int n;
        cin >> n;
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++)
                cin >> w[i][j];
        memset(f, 0x3f, sizeof f);   
        f[1][0] = 0; //表示从0到0,有一条路径
        
        for(int i = 0; i < 1 << n; i++) // 一共有1<<n个状态路径
            for(int j = 0; j < n; j++) // j表示走到那个点
                if(i>>j&1) //表示i经过j,进行状态转移
                    for(int k = 0; k < n; k++) // k表示到j点途中经过k点
                        if(i>>k&1) //表示经过k , 因为hamilton路径必须是经过每一个点的路径,
                                   //所以经过k的路径进行状态转移,不经过的不需要进行状态转移
                            f[i][j] = min(f[i][j], f[i - (1<<j)][k] + w[k][j]); //状态转移含义:
                                                                                //表示途径k点,再由k点到达j的最小Hamilton路径
        cout << f[(1<<n) - 1][n-1];
        return 0;
    }
    

树形DP

285. 没有上司的舞会

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int H[N]; // 开心指数
int f[N][2]; // 状态表示,f[i][0]表示选择不选择i时的最大快乐指数,f[i][1]表示选择i时的最大快乐指数
int h[N], idx, e[N], ne[N]; // 构建邻接表来表示身份树,根节点是最高的上司,依次向下
bool Father[N]; // Father[i]表示节点i有没有父亲节点
void add(int a, int b)  // 添加一条边a->b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u) //遍历u的所有子节点
{
    f[u][1] = H[u]; // 先为f[u][1]赋值
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i]; //j为子节点的值
        dfs(j); //先进行深度优先遍历,就算出叶子节点的快乐值,然后依次向上计算
        f[u][1] += f[j][0];   // 状态计算分两种情况:1. 选择父节点,所以子节点都不能选择,将所有不选择子节点的快乐指数相加
        f[u][0] += max(f[j][0], f[j][1]);  // 2. 不选择父节点,所以子节点可以选择也可以不选择,进行比较选最大值,并将所有子节点快乐指数相加
    }
}
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++)
        cin >> H[i];      
    memset(h, -1, sizeof h); // 将邻接表初始化为以-1为终止节点
    for (int i = 1; i <= n - 1; i ++ )
    {
        int a, b;
        cin >> a >> b;
        Father[a] = true;
        add(b, a);
    }
    int root = 0; // 将根节点初始化为0
    for(int i = 1; i <= n; i++)  // 查询根节点
        if(Father[i] == false)
        {
            root = i;
            break;
        }
    dfs(root);   
    cout << max(f[root][0], f[root][1]);
    return 0;
}

记忆化搜索

901. 滑雪

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int f[N][N]; // 状态表示:f[i][j]表示为从(i,j)出发一共有多少路径,状态属性:求最大值
//状态计算:一共有4个方向可以走:上下左右
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};
int w[N][N]; // 每个区域的高度
int n, m;

int dp(int px, int py)
{
    if(f[px][py] != -1) return f[px][py]; // 优化操作,表明f[px][py]已经计算过可以直接返回
    f[px][py] = 1;
    for(int i = 0; i < 4; i++)
    {
        int x = px + dx[i];
        int y = py + dy[i];
        if(x >= 1 && x <= n && y >= 1 && y <= m)
        {
            if(w[x][y] < w[px][py])
            {    
                f[px][py] = max(f[px][py], dp(x,y) + 1);
                
            }
        }
    }
   
    return f[px][py];
}
int main()
{    
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> w[i][j];
    int res = 0;   
    memset(f, -1, sizeof f); // 将状态表示初始化为-1 ,表明没有经过任何计算
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j++)
            res = max(dp(i, j), res);         
    cout << res;
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值