C++动态规划

来源:互联网 发布:2016网络自制剧排行榜 编辑:程序博客网 时间:2024/06/04 22:52

递推 也是蓝桥杯比赛中经常被使用的一种简单算法。递推 是一种用若干步可重复的简单运算来描述复杂问题的方法。


递推的特点在于,每一项都和他前面的若干项有一定关联。可以通过前面的若干项得出此项的数据。这种关系一般可以通过 递推关系式 来表示。


对于递推问题的求解一般从初始的一个或若干个数据项出发,通过递推关系式逐步推进,从而得出想要的结果,这种求解问题的方法叫 递推法。其中,初始的若干数据项称为边界。
我们来看这样一道题目:


已知一对兔子,每个月可以生一对小兔子,小兔子出生后的第二个月会变为成年兔子,会继续生小兔子。




1
第一个月,我们有 1 对小兔子。
2
第二个月,我们有 1 对成年的兔子。
3
第三个月,我们有 1 对成年的兔子,有 1 对小兔子,共 2 对。
4
第四个月,我们有 2 对成年的兔子,有 1 对小兔子,共 3 对。
5
第五个月,我们有 3 对成年的兔子,有 2 对小兔子,共 5 对。
6
......
现在,我们希望知道第 n个月,一共有多少只兔子。(兔子身体素质好,不会死掉。)


有可能你已经发现了规律了,但是我们还是要来理智地分析一下。


设第 i 个月的兔子数量为 F_i ,第 i月的成年兔子数量为 a_i,第 i月的小兔子数量为 b_i
​​ ,那么 F_i = a_i + b_i.


第 i 月的成年兔子,由第 i−1 个月的小兔子长大与第 i−1 月的成年兔子得到,也就是说,第 i−1 个月兔子的总数量 a_{i-1} + b_{i-1} = F_{i-1}

第 i个月的小兔子,是由第 i−1 个月的成年兔子生的,也就是第 i−2 个月的成年兔子和小兔子,也就是 i−2 个月兔子的总数量, a_{i-2} + b_{i-2} = F_{i-2}


这样,我们就找到了这个题目的递推关系式。如果用 F_i
​​  代表第 i 个月兔子的总数量,那么递推关系式为:F_i = F_{i-1} + F_{i-2}
​​ 边界值,我们知道 
​​ 那么,我们就可以写出这个题目的代码了。




1
f[1] = 1, f[2] = 1;
2
for(int i = 3;i <= n; ++i) {
3
    f[i] = f[i-1] + f[i-2];
4
}
5
//f[n] 即为所求。
通过解决这道题目,我们已经大致了解递推是一种怎样的思路了。递推的核心就是找到递推关系式。
我们现在来看一个经典的二维递推题目(NOIP2002)。

A 点有一个过河卒,需要走到目标 B 点。卒行走规则:可以向下、或者向右。同时在棋盘上的任一点有一个对方的马(如上图的 C 点),该马所在的点和所有跳跃一步可达的点称为对方马的控制点。例如上图 C 点上的马可以控制 9 个点,卒不能通过对方马的控制点。


棋盘用坐标表示,A 点(0,0)、B 点(n,m)、C 点(c_x​ ,c_y )现在要求你计算出过河卒从 A 点能够到达 B点的路径的条数。注:象棋中马走“日”。
如果你看到这个题目的第一反应是直接去搜索从 A 点到 B 的所有路径,那么计算速度会很慢,你的程序无法在规定时间内算出这个问题。我们要用递推的方式来解决它。


根据题目的要求,卒只能向下或者向右走,那么想要到达棋盘上的一个点,有两种方式:从左边的格子过来,或者从上边的格子过来。所以,过河卒到达某点的路径数目等于到达与其相邻的左边点和上边点的路径数目和。我们用 F_{i,j}
​​  来表示到达点 (i,j) 的路径数目。所以递推式为:F_{i,j} = F_{i-1,j} + F_{i, j-1}我们根据递推式发现,我们可以用逐行或逐列的递推方法求出从起点到终点的路径数目。我们来想一下边界条件,因为(0,0)是卒的起始位置,那么 F_{0,0} = 1我们在不考虑马控制点的情况下,可以写出递推代码。




1
f[0][0] = 1;
2
for (int i = 0;i <= n; ++i) {
3
    for (int j = 0; j <= m; ++j) {
4
        if (i != 0) {
5
            f[i][j] = f[i][j] + f[i-1][j];
6
        }
7
        if (j != 0) {
8
            f[i][j] = f[i][j] + f[i][j-1];
9
        }
10
    }
11
}
12
// f[n][m]即为点(n,m)的路径数目。
我们再想一下如何处理马控制的点。马控制的点的路径数一定是 0,这样其实并不影响我们做递推,只要遇到了马控制的点,直接设置为 0 就可以了。所以,我们需要提前标记出哪些点是马控制的点。我们可以用俩个一维数组来表示马的横向位移和纵向位移,这样来方便我们计算马的控制点,然后使用一个二维数组来标记某个点是否是马控制的点。




1
int cx, cy; //马的坐标
2
int x[8] = {1, 1, 2, 2, -1, -1, -2, -2}; //横向位移
3
int y[8] = {2, -2, 1, -1, 2, -2, 1, -1}; //纵向位移
4
int d[30][30]; //用来记录是否是马控制点
5
for (int i = 0; i < 30; ++i) { //将 d 数组初始化为 0
6
    for (int j = 0;j < 30; ++j) {
7
        d[i][j] = 0;
8
    }
9
}
10
d[cx][cy] = 1; // 用 1 来表示该点为马控制点
11
for (int i = 0; i < 8; ++i) {
12
    int tx = cx + x[i]; //计算马控制点横坐标
13
    int ty = cy + y[i]; //计算马控制点纵坐标
14
    if (tx >= 0 && tx <= n && ty >= 0 && ty <= n) {
15
        d[tx][ty] = 1; //记录为马控制点
16
    }
17
}
现在,我们只要把马控制点的因素加入到递推过程中就能写出完整的代码了。

动态规划 是编程解题的一种重要手段。1951年美国数学家 R.Bellman 等人,根据一类多阶段问题的特点,把多阶段决策问题变换为一系列互相联系的单阶段问题,然后逐个加以解决。与此同时,他提出了解决这类问题的“最优化原理”,从而创建了解决最优化问题的一种新方法:动态规划。
动态规划的基本概念:


阶段:把所给问题的求解过程恰当地分成若干个相互联系的阶段,以便于求解。过程不同,阶段数就可能不同。描述阶段的变量称为阶段变量常用 kk 表示。阶段的划分,一般是根据时间和空间的自然特征来划分,但要便于把问题的过程转化为多阶段决策的过程。
状态:状态表示每个阶段开始面临的自然状况或客观条件,它不以人们的主观意志为转移,也称为不可控因素。通常一个阶段有若干个状态,状态通常可以用一个或一组数来描述,称为状态变量。
决策:表示当过程处于某一阶段的某个状态时,可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。不同的决策对应着不同的数值,描述决策的变量称决策变量。
状态转移方程:动态规划中本阶段的状态往往是上一阶段的状态和上一阶段的决策的结果,由第 i 段的状态f(i) ,和决策 u(i) 来确定第 i+1 段的状态。状态转移表示为 F(i+1) = T(f(i),u(i)),称为状态转移方程。
策略:各个阶段决策确定后,整个问题的决策序列就构成了一个策略,对每个实际问题,可供选择的策略有一定范围,称为允许策略集合。允许策略集合中达到最优效果的策略称为最优策略。
动态规划必须满足最优化原理与无后效性。


最优化原理:“一个过程的最优决策具有这样的性质:即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。也就是说一个最优策略的子策略,也是最优的。
无后效性:如果某阶段状态给定后,则在这个阶段以后过程的发展不受这个阶段以前各个状态的影响。
来看一道题目。


蒜头君要回家,已知蒜头君在 (1,1)位置,家在 (n,n)坐标处。蒜头君走到一个点 (i,j) 会花费一定的体力 a_{ij}
​​ ,而且蒜头君只会往家的方向走,也就是只能往上,或者往右走。蒜头君想知道他回到家需要花费的最少体力是多少。


例如下图所示,格子中的数字代表走到该格子花费的体力:


对于该图来说,最优策略已在图上标出,共花费体力为:3 + 2 + 4 + 3 = 12.


我们把走到一个点看做一个状态,蒜头君想,我走到一个点只有两种方式,一个是从下面走到该点,一种是从左边走到该点。那么点 (i,j)要么是从 (i-1,j)走到 (i,j),要么是从点 (i,j-1) 走到 (i,j)。


所以从哪个点走到 (i,j)就是一个决策,我们用 dp(i,j) 来代表走到点 (i,j) 一共花费的最少体力。


我们需要花费最少力气走到家,所以可以得到状态转移方程:dp(i,j) = min(dp(i-1,j), dp(i,j-1)) + a_{ij}dp(i,j)=min(dp(i−1,j),dp(i,j−1))+a
​ij
​​ 。根据转移方程我们可以推出走到每个点花费的最少体力。


对于图中的边界点,要在转移前加上判断是否为边界,如:点 (1,3) 只能从点 (1,2) 走过来,点 (3,1) 只能从点 (2,1) 走过来。
动态规划的题目的核心是写出状态转移方程,对于一个动态规划的题目,如果我们能写出转移方程那么代码实现就变得简单多了。大部分的动态规划题目,在计算出转移方程后,可以用类似于递推的循环嵌套,来写出代码。


主要代码:




1
int a[100][100]; // a数组代表走到点(i,j)花费的体力
2
int dp[100][100]; // dp数组代表走到点(i,j)一共花费的最少体力
3
dp[1][1] = 0;
4
for (int i = 1; i <= n; ++i) {
5
    for (int j = 1;j <= n; ++j) {
6
        if (i == 1 && j == 1) {
7
            continue;
8
        } else if (i == 1) { //边界点
9
            dp[i][j] = dp[i][j-1] + a[i][j];
10
        } else if (j == 1) { //边界点
11
            dp[i][j] = dp[i-1][j] + a[i][j];
12
        } else {
13
            dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + a[i][j]; //转移方程
14
        }
15
    }
16
}

0 0
原创粉丝点击