算法 | 动态规划 | 系列题目讲解(思路记录part.1)

字符串分割

  • 问题描述

给定一个字符串和一个词典dict,确定s是否可以根据词典中的词分成
一个或多个单词。
比如,给定
s = “leetcode”
dict = [“leet”, “code”]
返回true,因为"leetcode"可以被分成"leet code"

题目链接

(说明一下,F{i)中的i是指字符串的前i个字符,和str[i]不同,这里的i是数组下标,所以str[0, i]是指字符串的前i + 1个字符)

首先确定状态,F(i)表示字符串的前i个字符能否被分割,用true和false表示。现在要确定的是如何推导F(i)?假设字符串为str,我们要确定str的前i个字符是否可以分割,分解子问题,用j分解字符串,确定str[0, j - 1]与str[j, i - 1]是否可以分割,它们可以分割str就可以分割。因为str[0, j - 1]是字符串的前j个字符,我们可以通过F(j)获取其能否被分割(j肯定比i小,F(j)的状态肯定已经更新过了),至于[j, i - 1]区间的字符,就要遍历字典,查找str[j, i - 1]是否在字典中。如果两者的结果都是true,那么str的前i个字符就是可以分割的。

既然F(i)表示的是字符串的前i个字符能否被分割,那么F(0)是什么?我们将其设置为true,字符串的前0个字符是不存在的,但是我们需要F(0)作为初始状态进行递推。假设字符串的长度为size,那么最后只要返回字符串的前size个字符能否被分割,即F(size)就可以了

bool wordBreak(string s, unordered_set<string> &dict) {
    vector<bool> can_break(s.size() + 1, false);
    can_break[0] = true;
    // 从第1个字符开始到最后一个字符结束,确定状态
    for (size_t i = 1; i <= s.size(); ++i)
    {
        // 从j分割字符串,j表示字符串的第j个字符,不是数组下标
        for (size_t j = 0; j <= i; ++j)
        {
        	// 前j个字符可以分割,并且j + 1到i的字符也可以分割
            if (can_break[j] && dict.count(s.substr(j, i - j)))
            {
                can_break[i] = true;
                break;
            }
        }
    }
    //  返回数组的最后一个位置
    return can_break[s.size()];
}

其中,在字典中查找会用到count接口,获取字符串的子串会用到substr接口,这些接口可以在这里查询

三角矩阵

  • 问题描述

给定一个三角矩阵,找出自顶向下的最短路径和,每一步可以移动到下一行的相邻数字。

题目链接

在这里插入图片描述

题目会给定一个二维数组,用二维数组表示一个三角形。首先确定状态,F(i, j)表示从顶点开始到i行j列的点的最短路径,求出从顶点到最后一行所有点的最短路径,在其中选择一个最小的,就是自顶向下的最短路径和。现在要确定的是如何推导F(i, j)?题目说每一步可以移动到下一行的相邻数字,也就是可以从dp[i - 1, j -1]或者dp[i - 1, j]移动到dp[i, j],从这两条路径中选择较小的那条,即F(i, j) = min(F(i - 1, j - 1), F(i - 1, j)) + triangle[i][j]。比如要到达5,可以从3到5,也可以从4到5,我们要从这两条路径中找出较小的那条(在dp数组中查找),然后再加上自己的值,就是从顶点到5的最短路径。更新完dp数组,在数组的最后一行查找得到的最小值就是最后的答案

但是数组的初始状态需要格外注意,首先是顶点到顶点的最短路径就是自己,所以dp[0][0]为0。其次是三角矩阵的左边界和右边界,比如要达到4,只能从6走,达到4的路径只有一条,顶点2->3->6->4,同理边界上所有的点都是这样,此时我们的状态转移方式对这些点不再适用,我们需要单独处理这些点,它们的最短路径就是从顶点不断的累加到自己。

int minimumTotal(vector<vector<int> > &triangle) {
    size_t row = triangle.size();
    // 数组初始化
    for (size_t i = 1; i < row; ++i)
    {
        triangle[i][0] += triangle[i - 1][0];
        triangle[i][triangle[i].size() - 1] += triangle[i - 1][triangle[i - 1
        ].size() - 1];
    }

    // 更新数组
    for (size_t i = 1; i < row; ++i)
    {
        for (size_t j = 1; j < triangle[i].size() - 1; ++j)
        {
            triangle[i][j] += min(triangle[i - 1][j - 1], triangle[i - 1][j]);
        }
    }

    // 查找最小路径
    int ret = triangle[row - 1][0];
    for (size_t i = 0; i < triangle[row - 1].size(); ++i)
    {
        ret = min(ret, triangle[row - 1][i]);
    }
    return ret;
}

写这道题时,最难的地方不是转移方程的推导,而是数组边界的确定,左右边界的更新,i,j作为数组下标具体的范围在哪?这些都要仔细考虑。

除了自顶向下的更新数组,我们还可以自底向上更新,用F(i, j)表示i行j列的点到底边的最短路径,不再是从顶点到i行j列的点的最短路径了。要更新F(i, j)就要考虑F(i + 1, j)与F(i + 1, j + 1),因为i行j列的点到底边只经过了这两个点,所以状态转移方程F(i, j) = min(F(i + 1, j), F(i + 1, j + 1)) + triangle(i, j),并且每一次对i行的更新都要用到i + 1行的数据(转移方程的右边都是i + 1),所以我们需要自底向上开始更新,使i + 1行的数据满足我们设定的状态,再更新i行的数据。

那么初始状态是怎样的?因为最后一行到底边的最短距离就是自己本身,所以最后一行的状态已经确定好了,我们从倒数第二行开始更新数组,并且左右边界也不需要特地更新,因为这些点已经满足了我们的状态转移方程。最后我们只要返回F(0, 0)即可,F(0, 0)为从顶点到底边的最短距离

int minimumTotal(vector<vector<int> > &triangle) {
    for (int i = triangle.size() - 2; i >= 0; --i)
    {
        for (int j = 0; j < triangle[i].size(); ++j)
        {
            triangle[i][j] += min(triangle[i + 1][j], triangle[i + 1][j + 1]);
        }
    }
    return triangle[0][0];
}

很显然,这样的解法更简单

路径总数

  • 问题描述

在一个m*n的网格的左上角有一个机器人,机器人在任何时候只能向下或者向右移动,机器人试图到达网格的右下角,有多少可能的路径。

题目链接

同样的,先确定题目的状态F(i, j),F(i, j)表示从左上角到i行j列的点的总路径数。那么F(i, j)要怎么推导呢?从题目中我们得知,机器人只能向下或者向右移动,也就是说要走到[i, j],必须要经过[i - 1, j]或者[i, j - 1],所以F(i, j) = F(i - 1, j) + F(i, j - 1),到[i, j]的路径数等于[i - 1, j]加上[i, j - 1]的路径总和。状态转移方程确定了,现在要确定的就是题目的初始条件,用来更新的dp数组中,由于第一行和第一列的所有点不满足该状态转移方程,所以我们应该先处理这些点,从左上角到这些点的路径只有一条,要么直接向右走,要么直接向下走,所以dp数组的第一行和第一列被初始化为1。最后返回F(m, n)就行了

int uniquePaths(int m, int n) {
    vector<vector<int>> dp(m, vector<int>(n));
    // dp数组的初始化    
    for (size_t i = 0; i < m; ++i)
    {
        dp[i][0] = 1;
    }
    for (size_t i = 0; i < n; ++i)
    {
        dp[0][i] = 1;
    }
	// dp数组的更新
    for (size_t i = 1; i < m; ++i)
    {
        for (size_t j = 1; j < n; ++j) 
        {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }

    return dp[m - 1][n - 1];
}

最小路径和

  • 问题描述

给定一个m*n的网格,网格用非负数填充,找到一条从左上角到右下角的最短路径(只能向下或者向右移动)。

题目链接
和路径总数那题类似,先确定状态,F(i, j)表示从左上角到第i行j列的最小路径和,当i和j指向右下角时,F(i, j)就是从左上角到右下角的最短路径。现在要确定的是状态转移方程,F(i, j) = min(F(i - 1, j), F(i, j - 1)) + array[i, j]:从唯二到达[i, j]的路径中,选择一条更小的,并加上到[i, j]的值。然后就是确定初始状态,第一行与第一列的点只有一种方向可以到达,直接向右或者直接向下走就行,所以在更新dp数组前先更新这些点

int minPathSum(vector<vector<int> >& grid) {
    size_t row = grid.size();
    size_t col = grid[0].size();
    // dp数组的初始化
    for (size_t i = 1; i < row; ++i)
    {
        grid[i][0] += grid[i - 1][0];
    }
    for (size_t i = 1; i < col; ++i)
    {
        grid[0][i] += grid[0][i - 1];
    }
    // dp数组的更新
    for (size_t i = 1; i < row; ++i)
    {
        for (size_t j = 1; j < col; ++j)
        {
            grid[i][j] += min(grid[i - 1][j], grid[i][j - 1]);
        }
    }
    return grid[row - 1][col - 1];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值