动态规划
多阶段决策的过程:每一步求解的问题是后面阶段求解问题的子问题,每一步决策都将依赖于以前决策的结果
优化函数值之间存在依赖关系
优化函数的特点:任何最短路的子路径相对于子问题始、终点最短
一定要存在依赖关系!
一定要满足优化原则或最优子结构性质
设计要素
多阶段决策过程,每步处理一个子问题,界定子问题的边界
列出优化函数的递推方程以及初值
问题满足优化原则或最优子结构性质。一个最优决策序列的任 何子序列本身一定是相对于子序列的初始和结束状态的最优决策序列。
例题1——矩阵链乘法
动态规划算法设计要素:(《算法设计与分析 屈婉玲》)
- 划分子问题,用参数表达子问题的边界,将问题求解转变为多步判断的过程。
- 确定优化函数,以函数的极大(或极小)作为判断依据,确定是否满足优化原则
- 列出关于优化函数的递推方程(或不等式)和边界条件
- 考虑是否需要设置标记函数,以记录划分位置
- 自底向上计算,以备忘录方式存储中间结果
- 根据备忘录(和标记函数)追溯给出的最优解
描述
设A1,A2,...AnA_1,A_2,...A_nA1,A2,...An 为矩阵序列,AiA_iAi为Pi−1∗PiP_{i-1}*P_iPi−1∗Pi阶矩阵,i = 1,2,3…n.试确定矩阵的乘法顺序,使得元素相乘的总次数最少。
输入:向量P=<P0,P1...Pn>P = <P_0,P_1...P_n>P=<P0,P1...Pn>其中P0,P1...Pn为n个矩阵的行数与列数P_0,P_1...P_n为n个矩阵的行数与列数P0,P1...Pn为n个矩阵的行数与列数
输出:最小的乘法次数以及矩阵链乘法加括号的位置。
样例:
input: P=<30,35,15,5,10,20> n=5
output: 11875 3,1
输出的意义表示:A1∗A2∗A3∗A4∗A5A_1*A_2*A_3*A_4*A_5A1∗A2∗A3∗A4∗A5以A1(A2∗A3))(A4∗A5)A_1(A_2*A_3))(A_4*A_5)A1(A2∗A3))(A4∗A5)形式相乘,乘法计算次数最低为11875次
分析
对于A1...nA_{1...n}A1...n的矩阵链,其任一划分之后,会出现两个子问题A1...k和Ak+1....nA_{1...k}和A_{k+1....n}A1...k和Ak+1....n而我们需要计算的是两个子问题。对于每个子问题 都有矩阵链的前后两个边界,对于A1...kA_{1...k}A1...k来说前边界是1后边界是k。我们令m[i,j]来表示矩阵链Ai...jA_{i...j}Ai...j的最优解。那么假设在i到j的任意位置划分,得到Ai...k和Ak+1...nA_{i...k}和A_{k+1...n}Ai...k和Ak+1...n。那么Ai...jA_{i...j}Ai...j的最优解就依赖于两个子问题。这种依赖关系写成递推方程就是:
m[i,j]={0i=jmini≤k<jm[i,k]+m[k+1,j]+Pi−1PkPji<j m[i,j] = \begin{cases} 0 & i=j \\ \min_{i \le k<j}{m[i,k]+m[k+1,j]+P_{i-1}P_kP_j} & i<j \\ \end{cases} m[i,j]={0mini≤k<jm[i,k]+m[k+1,j]+Pi−1PkPji=ji<j
递归方式伪码
迭代方式伪码
实现代码过程
import java.util.ArrayList;
public class MatrixMutilpy {
public static int p[] = {30,35,15,5,10,20};
// n 是数字的长度 而实际矩阵个数为 n-1
public static int n = p.length;
public static int m[][] = new int[n][n];
public static int s[][] = new int [n][n];
// 递归形式
// 递推方程为: m[i][j] = min{m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j]} i<= k <j
public static int recurMatrixChain(int []p,int i,int j){
if(j == i) {
m[i][j] = 0;
s[i][j] = i;
return m[i][j];
}
for (int k = i; k < j; k++) {
int q = recurMatrixChain(p,i,k)+recurMatrixChain(p,k+1,j)
+ p[i-1]*p[k]*p[j];
if (q < m[i][j]){
m[i][j] = q;
s[i][j] = k;
}
}
return m[i][j];
}
// 迭代实现
public static int IteratorMatrixChain(int p[]){
// 提前都 m[][] = 0 相当于 处理了r=1 的情况
// r 取值 为 2 3 4 5 < n=6 r 表示矩阵链规模,r=2 表示 A1*A2 A2*A3 r=3 表示 A1*A2*A3
for (int r = 2; r < n;r++){
// 以 r=2 为例 n-r+1 = 6-2+1 = 5 i取值为 1 2 3 4
// 含义就是 第几个链 r=2时: i=1 表示 A1*A2 i=2 表示A2*A3
for (int i = 1; i < n-r+1 ; i++) {
int j = i+r-1;
// 先计算一个 填到 m[k][l] 上 之后填的时候 比较大小
// 比如 r=3 i=1时j=3 A1*A2*A3 下面先计算了 A1(A2*A3)
// m[i][i] = 0 可以省去不写
m[i][j] = m[i][i] + m[i+1][j] + p[i-1]*p[i]*p[j];
s[i][j] = i;
// 上边相等于计算了 k=i的情况 下面k 从i+1开始;
// 到j-1
for (int k = i+1; k < j; k++) {
int temp = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if(temp < m[i][j]){
m[i][j] = temp;
s[i][j] = k;
}
}
}
}
return m[1][n-1];
}
public static ArrayList<Integer> find(){
ArrayList<Integer> result = new ArrayList<>();
int i = 1;
int j = n-1;
while(true){
int t = s[i][j];
result.add(t);
j = t;
if(i == j) break;
}
return result;
}
public static void main(String[] args) {
MatrixMutilpy test = new MatrixMutilpy();
for (int k = 0; k < n; k++) {
for (int l = 0; l < n ; l++) {
m[k][l] = Integer.MAX_VALUE;
s[k][l] = 0;
}
}
int result = test.recurMatrixChain(p,1,p.length-1);
System.out.println(result);
for (int k = 0; k < n; k++) {
for (int l = 0; l < n ; l++) {
m[k][l] = 0;
s[k][l] = k;
}
}
MatrixMutilpy test1 = new MatrixMutilpy();
int result2 = test1.IteratorMatrixChain(p);
System.out.println(result2);
ArrayList<Integer> res = find();
System.out.println(res);
}
}
例题2——投资问题
描述
设 我们现在有m元钱,n项投资,函数fi(x)f_i(x)fi(x)表示将x元投入第i个项目所产生的效益,i=1,2...ni=1,2...ni=1,2...n,问如何分配这m元钱,使得总的投资效益最高?
假设钱数的分配都是非负整数,分配给第i个项目的钱数是xix_ixi,那么该问题可以描述为:
目标函数:max{f1(x1)+f2(x2)+⋯+fn(xn)}约束条件:x1+x2+⋯+xn=m,xi∈N
目标函数:\max \left\{f_{1}\left(x_{1}\right)+f_{2}\left(x_{2}\right)+\cdots+f_{n}\left(x_{n}\right)\right\} \\
约束条件:x_1+x_2+\cdots + x_n = m ,x_i \in N
目标函数:max{f1(x1)+f2(x2)+⋯+fn(xn)}约束条件:x1+x2+⋯+xn=m,xi∈N
实例
子问题的界定和计算顺序
子问题界定:我们正常的想法K个项目,可以看看投前1个项目时候的收益,投前2个项目的…一直到前5个项目(全部项目)的最大收益,而对于前k个项目的收益,这里还需要对投入的钱再进行细分,前一个项目的时候,投入x元的最大收益(x=1,2…m)。这里也就出现了两个参数K和x。
在矩阵链中,我们的两个参数i和j都是同一个含义,就是矩阵的位置下标。这里我们的k和x是代表着不同的含义。
如果令Fk(x)F_k(x)Fk(x)表示x万元投入到前k个项目中,我们可以获得的最大收益。首先,我们从X万元中,分配xkx_kxk万元给第k个项目,那么剩下的x−xkx-x_kx−xk万元,就给前k−1k-1k−1个项目,而前k−1k-1k−1的最佳分配方案已经计算过,那么我们也就得到了递推方程:
Fk(x)=max0≤xk<x{fk(xk)+Fk−1(x−xk)},k=2,3,⋯ ,nF1(x)=f1(x),Fk(0)=0,k=1,2,⋯ ,n
\begin{array}
{l}{F_{k}(x)=\max _{0\le x_{k}<x}\left\{f_{k}\left(x_{k}\right)+F_{k-1}\left(x-x_{k}\right)\right\}, \quad k=2,3, \cdots, n} \\
{F_{1}(x)=f_{1}(x), \quad F_{k}(0)=0, \quad k=1,2, \cdots, n}
\end{array}
Fk(x)=max0≤xk<x{fk(xk)+Fk−1(x−xk)},k=2,3,⋯,nF1(x)=f1(x),Fk(0)=0,k=1,2,⋯,n
这个问题的初始化是F1(x)=f1(x)F_1(x) = f_1(x)F1(x)=f1(x)即得到了F1(x)F_1(x)F1(x)的第一列。
F1(x)=11,F2(x)=12,F3(x)=13,F4(x)=14,F5(x)=15F_1(x)=11,F_2(x)=12,F_3(x)=13,F_4(x)=14,F_5(x)=15F1(x)=11,F2(x)=12,F3(x)=13,F4(x)=14,F5(x)=15
根据递推公式:
F2(x)=max0≤x2<x{f2(x2)+F2−1(x−x2)} =max0≤x2<x{f2(x2)+F1(x−x2)} F_2(x) = \max_{0 \le x_2 < x}\{f_2(x_2)+F_{2-1}(x-x_{2})\} \\ \ \ \ =\max_{0 \le x_2 < x}\{f_2(x_2)+F_{1}(x-x_{2})\} F2(x)=0≤x2<xmax{f2(x2)+F2−1(x−x2)} =0≤x2<xmax{f2(x2)+F1(x−x2)}
F2(1)=max{f2(0)+F1(1),f2(1)+F1(0)}=11 F_2(1) = max\{f_2(0)+F_1(1),f_2(1)+F_1(0)\} = 11 F2(1)=max{f2(0)+F1(1),f2(1)+F1(0)}=11
F2(2)=max{f2(0)+F1(2),f2(1)+F1(1),f2(2)+F1(0)}=12 F_2(2) = max\{f_2(0)+F_1(2),f_2(1)+F_1(1),f_2(2)+F_1(0)\} = 12 F2(2)=max{f2(0)+F1(2),f2(1)+F1(1),f2(2)+F1(0)}=12
F2(3)=max{f2(0)+F1(3),f2(1)+F1(2),f2(2)+F1(1),f2(3)+F1(0)}=16F_2(3) = max\{f_2(0)+F_1(3),f_2(1)+F_1(2),f_2(2)+F_1(1),f_2(3)+F_1(0)\} = 16 F2(3)=max{f2(0)+F1(3),f2(1)+F1(2),f2(2)+F1(1),f2(3)+F1(0)}=16
依次类推,可以得到F2(4),F2(5)F_2(4),F_2(5)F2(4),F2(5)
在计算F3(x)F_3(x)F3(x)的时候,只会考虑f3(x3)+F2(x−x3)f_3(x_3)+F_2(x-x_3)f3(x3)+F2(x−x3) 即与F1(x)F_1(x)F1(x)无关。依次类推计算F4,F5F_4,F_5F4,F5。
代码实现
public class Investment {
public static int[][] mem;
public static int[][] tag;
private static int compute_max_invest(int[][] invest,int n,int k) {
// i 表示 前 i个 项目 i=0是第一个项目,已经初始化 所以从i=1开始
for (int i = 1; i < k; i++) { // 1 2 3
// j 来表示 分配j万元
for (int j = 1; j < n; j++) { // 0 1 2 3 4 5
// 当分配j万元时候 在各种组合中的求最大
int max = Integer.MIN_VALUE;
for (int l = 0; l <= j; l++) {
// F_k(x) = max{ f_k(x_k) + F_{k-1}(x-x_k) }
// temp = 第i个项目投入l万元 + 前i-1个项目投入j-l万元
int temp = invest[l][i]+mem[j-l][i-1];
if (temp >= max){
mem[j][i] = temp;
max = temp;
tag[j][i] = l;
}
}
}
}
return mem[n-1][k-1];
}
public static void trace_result(int res[],int k,int n){
int total = n-1;
// k 是项目数
for (int i = k-1; i >= 0; i--) {
int temp = tag[total][i];
total -= temp;
res[i] = temp;
}
}
public static void main(String[] args) {
// 输入是一个n*k的矩阵
int invest[][] ={{0,0,0,0},{11,0,2,20},{12,5,10,21},{13,10,30,22},{14,15,32,23},{15,20,40,24}};
int n = invest.length; // 行数 表示投入 n万元 n=6 实际上是 0 1 2 3 4 5
int k = invest[0].length; // 列数 表示 共多少个项目
mem = new int[n][k];
tag = new int[n][k];
System.out.println("输入数据为");
for (int i = 0; i < n; i++) {
for (int j = 0; j < k; j++) {
System.out.printf("%d ",invest[i][j]);
}
System.out.println();
}
// 初始化 将 第一个项目的资金 放入 备忘录。
// 标记函数
for (int i = 0; i < n; i++) {
mem[i][0] = invest[i][0];
tag[i][0] = i;
}
int result[] = new int[k];
int max_invest = compute_max_invest(invest,n,k);
System.out.printf("max_investion:%d\n",max_invest);
System.out.println("备忘录如下:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < k; j++) {
System.out.printf("%d ",mem[i][j]);
}
System.out.println();
}
System.out.println("追踪解如下:");
trace_result(result,k,n);
for (int i = 0; i < k; i++) {
System.out.println(result[i]);
}
}
}
例题3——背包问题
描述
一个背包,可以放入n种物品,物品j的重量和价值分别为wj,vj,j=1,2,⋯ ,nw_j,v_j,j=1,2,\cdots,nwj,vj,j=1,2,⋯,n,如果背包的最大重量限制是b,怎么样选择放入背包的物品以使得背包的总价值最大?
组合优化问题,设xjx_jxj表示装入背包的第j个物品的数量,解可以表示为<x1,x2,⋯ ,xn><x_1,x_2,\cdots,x_n><x1,x2,⋯,xn>。那么目标函数和约束条件是:
目标函数:max∑j=1nvjxj约束条件:{∑j=1nwjxj≤bxj∈N
目标函数:max\sum_{j=1}^{n}v_jx_j \\
约束条件:\begin{cases}
\sum_{j=1}^{n}w_jx_j \le b \\
x_j \in N
\end{cases}
目标函数:maxj=1∑nvjxj约束条件:{∑j=1nwjxj≤bxj∈N
如果组合优化问题的目标函数和约束条件都是线性函数,称为线性规划。如果线性规划问题的变量xjx_jxj都是非负整数,则称为整数规划问题。背包问题就是整数规划问题。限制所有的xj=0 or xj=1x_j=0 \ or \ x_j=1xj=0 or xj=1时称为0-1背包
子问题的界定(就是靠什么来划分子问题):由参数k和y界定
k:考虑对物品1,2,3,…,k的选择。
y:表示背包总重
子问题计算顺序:k=1,2,…,k,对给定的k,y=1,2,…,b
Fk(y)F_k(y)Fk(y):装前k个物品,重量不超过y时的背包最大值。
Fk(y)F_k(y)Fk(y)包含两种情况:不装第k种物品或至少装一件第k种物品。
对Fk(y−wk)+vkF_k(y-w_k)+v_kFk(y−wk)+vk的解释:装一件第k种物品后,最优的解法仍然是在前k个物品进行选择,仍有可能再选入1件第k种物品。
对边界条件:
F1(y)=⌊yw1⌋v1F_1(y) = \lfloor\frac{y}{w_1}\rfloor v_1F1(y)=⌊w1y⌋v1:即只用第一种物品背包重量限制为y的最大价值,为了保证背包不超重,第一种物品至多能装⌊yw1⌋\lfloor\frac{y}{w_1}\rfloor⌊w1y⌋个,因为背包价值为⌊yw1⌋v1\lfloor\frac{y}{w_1}\rfloor v_1⌊w1y⌋v1
Fk(y)=−∞,y<0F_k(y) = - \infty,\quad y<0Fk(y)=−∞,y<0 有些Fk(y−wk)<0F_k(y-w_k)<0Fk(y−wk)<0那么通过设置为负无穷,在选择过程中抛弃掉为负的情况。
标记函数:用来追踪解
ik(y):装前k种物品,总重不超过y,背包达到最大值时装入物品的最大标号ik(y)={ik−1(y)Fk−1(y)>Fk(y−wk)+vkkFk−1(y)≤Fk(y−wk)+vki1(y)={0y<w11y≥w1
i_k(y):装前k种物品,总重不超过y,背包达到最大值时装入物品的最大标号 \\
i_{k}(y)=\left\{\begin{array}{ll}{i_{k-1}(y)} & {F_{k-1}(y)>F_{k}\left(y-w_{k}\right)+v_{k}} \\ {k} & {F_{k-1}(y) \leq F_{k}\left(y-w_{k}\right)+v_{k}}\end{array}\right.\\
i_{1}(y)=\left\{\begin{array}{ll}{0} & {y<w_{1}} \\ {1} & {y \geq w_{1}}\end{array}\right.
ik(y):装前k种物品,总重不超过y,背包达到最大值时装入物品的最大标号ik(y)={ik−1(y)kFk−1(y)>Fk(y−wk)+vkFk−1(y)≤Fk(y−wk)+vki1(y)={01y<w1y≥w1
实例
n=4,b=10v1=1,v2=3,v3=5,v5=9w1=2,w2=3,w3=4,w4=7 n=4,b=10 \\ v_1=1,v_2=3,v_3=5,v_5=9 \\ w_1=2,w_2=3,w_3=4,w_4=7 \\ n=4,b=10v1=1,v2=3,v3=5,v5=9w1=2,w2=3,w3=4,w4=7
Fk(y)=max{Fk−1(y),Fk(y−wk)+vk}{F_{k}(y)=\max \left\{F_{k-1}(y), F_{k}\left(y-w_{k}\right)+v_{k}\right\}}Fk(y)=max{Fk−1(y),Fk(y−wk)+vk}
按照递推公式:以k=2为例子,简单演算如下:
F2(1)=max{F2−1(1),F2(1−w2)+v2}=max{0,−∞}=0{F_2(1)=\max \{F_{2-1}(1), F_{2}(1-w_{2})+v_{2}\}} = max\{0,-\infty\} = 0F2(1)=max{F2−1(1),F2(1−w2)+v2}=max{0,−∞}=0
F2(2)=max{F2−1(2),F2(2−w2)+v2}=max{1,−∞}=1{F_2(2)=\max \{F_{2-1}(2), F_{2}(2-w_{2})+v_{2}\}} = max\{1,-\infty\} = 1F2(2)=max{F2−1(2),F2(2−w2)+v2}=max{1,−∞}=1
F2(3)=max{F2−1(3),F2(3−w2)+v2}=max{1,3}=3{F_2(3)=\max \{F_{2-1}(3), F_{2}(3-w_{2})+v_{2}\}} = max\{1,3 \} = 3F2(3)=max{F2−1(3),F2(3−w2)+v2}=max{1,3}=3
F2(4)=max{F2−1(4),F2(4−w2)+v2}=max{2,3}=3{F_2(4)=\max \{F_{2-1}(4), F_{2}(4-w_{2})+v_{2}\}} = max\{2,3 \} = 3F2(4)=max{F2−1(4),F2(4−w2)+v2}=max{2,3}=3
F2(5)=max{F2−1(5),F2(5−w2)+v2}=max{2,1+3}=4{F_2(5)=\max \{F_{2-1}(5), F_{2}(5-w_{2})+v_{2}\}} = max\{2,1+3 \} = 4F2(5)=max{F2−1(5),F2(5−w2)+v2}=max{2,1+3}=4
F2(6)=max{F2−1(6),F2(6−w2)+v2}=max{2,3+3}=6{F_2(6)=\max \{F_{2-1}(6), F_{2}(6-w_{2})+v_{2}\}} = max\{2,3+3 \} = 6F2(6)=max{F2−1(6),F2(6−w2)+v2}=max{2,3+3}=6
依次类推,可得备忘录表:
标记函数的备忘录:
关于背包问题的总结
物品受限背包:第i种物品最多用nin_ini个
0-1背包问题:xi=0 or 1,i=1,2,⋯ ,nx_i = 0\ or\ 1,i=1,2,\cdots,nxi=0 or 1,i=1,2,⋯,n
多背包:m个背包,背包jjj装入最大重量Bj,j=1,2,⋯ ,mB_j,j=1,2,\cdots,mBj,j=1,2,⋯,m在满足所有背包重量约束下使物品价值最大。
二维背包:每件物品重量wiw_iwi和体积tj,i=1,2,⋯ ,nt_j,i=1,2,\cdots,ntj,i=1,2,⋯,n,背包总重不超过b,体积不超过V,使得物品价值最大。
代码实现
此问题是完全背包问题,即 一个物品可重复出现。
public class knapsackProblem {
public static int[][]mem; // 备忘录表
public static int[][]s; // 标记函数表
public static void main(String[] args) {
int n = 4;
int d = 10;
int []w = {2,3,4,7};
int []v = {1,3,5,9};
mem = new int[n+1][d+1];
s = new int[n+1][d+1];
// 默认初始化为0
int max_value = Completely_backpack(w,v,n,d);
System.out.printf("背包最大值为:%d\n",max_value);
System.out.printf("备忘录表为:\n");
for (int i = 0; i < n + 1; i++) {
for (int j = 0; j < d + 1; j++) {
System.out.printf("%d ",mem[i][j]);
}
System.out.println();
}
System.out.println("标记函数表尾:");
for (int i = 0; i < n + 1; i++) {
for (int j = 0; j < d + 1; j++) {
System.out.printf("%d ",s[i][j]);
}
System.out.println();
}
// 追踪解 且 初始化为 0
int []res = new int[n+1];
traceSolution(res,n,d,w);
System.out.println("背包装入各个物品的数量为:");
for (int i = 1; i < n + 1; i++) {
System.out.printf("%d ",res[i]);
}
}
public static int Completely_backpack(int []w,int []v,int n,int d){
// F_k(y) = max{F_{k-1}(y), F_k(y-w_k)+v_k }
// i表示 前i个 物品放入背包
for (int i = 1; i <= n; i++) {
// j 表示 背包重量为j
for (int j = 1; j <= d; j++) {
int not = mem[i-1][j];
// w[i-1]是因为 w下标从0 开始,而i从1开始
int in;
if (j-w[i-1] < 0){
in = Integer.MIN_VALUE;
}
else in = mem[i][j-w[i-1]] + v[i-1];
mem[i][j] = Math.max(not,in);
// 根据标记函数的定义来写
if (not > in){
s[i][j] = s[i-1][j];
}
else{
s[i][j] = i;
}
}
}
return mem[n][d];
}
public static void traceSolution(int []res,int n,int d,int []w){
int y = d;
for (int i = n; i >0 ;) {
int temp = s[i][y];
while(temp == i){
// i-1 符合w的下标
y = y-w[i-1];
res[i]++;
temp = s[i][y];
}
i = s[i][y];
}
}
}
例题4——01背包
题目描述(Leetcode 416)
Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.
Note:
- Each of the array element will not exceed 100.
- The array size will not exceed 200.
Example 1:
Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].
Example 2:
Input: [1, 2, 3, 5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.
解题分析
能否将数组分成两部分,且两部分彼此相等。
首先就是将元素都加起来,因为都是正整数,如果和为奇数,那么分成两部分怎么分两部分也不可能相等。
如果总和为偶数,那么问题就转变成 从数组中,挑出来相加等于总和一半的数,如果挑不出来那就是不存在,否则就是存在。(从一堆物品中挑出来装入背包,背包的总重为 总和的一半 )
建模:Fk(y)=max{Fk(y),Fk−1(y−wk)+wk}F_k(y) = max\{F_k(y),F_{k-1}(y-w_k)+w_k\}Fk(y)=max{Fk(y),Fk−1(y−wk)+wk}
Fk(y)F_k(y)Fk(y)表示选前k个数,且总和不超过y时可以加出来的和。当k=n,y=target时,Fk(target)=target当k=n,y=target时,F_k(target)=target当k=n,y=target时,Fk(target)=target那么也就是可以凑出来值为target的组合。若不等,则不存在。
题解
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n <= 0) return false;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += nums[i];
}
if (sum %2 == 1){
return false;
}
int target = sum >> 1;
int [][]dp = new int[n+1][target+1];
//
int y = target;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= target; j++) {
int left = dp[i-1][j];
int right;
if (j-nums[i-1] < 0){
right = Integer.MIN_VALUE;
}
else {
right = dp[i-1][j-nums[i-1]]+nums[i-1];
}
dp[i][j] = Math.max(left,right);
}
}
if (dp[n][target] == target)
return true;
return false;
}
大神解法
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if ((sum & 1) == 1) {
return false;
}
sum /= 2;
int n = nums.length;
boolean[] dp = new boolean[sum+1];
Arrays.fill(dp, false);
dp[0] = true;
for (int num : nums) {
for (int i = sum; i > 0; i--) {
if (i >= num) {
dp[i] = dp[i] || dp[i-num];
}
}
}
return dp[sum];
}
先说优点,再说其思路。
代码优点:for的迭代器写法,位操作判断奇偶,备忘录用二进制(节省存储还好用)
思路:只维护一个一维矩阵,长度为target+1
递推公式为:Fk(y)=Fk(y)∣∣F(y−wk)F_k(y) = F_k(y) || F(y-w_k)Fk(y)=Fk(y)∣∣F(y−wk)
Fk(y)F_k(y)Fk(y)表示,使用前k个数,能否凑出和为y。能则为true,不能则为false。子问题还是考y,k来界定的。
当k=n,y=target时,Fn(target)当k=n,y=target时,F_n(target)当k=n,y=target时,Fn(target)表示使用所有的数,能否凑出和为target。若能则返回true,否则false。
例题5——最长公共子序列
描述
给定序列
X=<x1,x2,x3,⋯ ,xn>Y=<y1,y2,y3,⋯ ,ym>
X=<x_1,x_2,x_3,\cdots,x_n> \\
Y=<y_1,y_2,y_3,\cdots,y_m>
X=<x1,x2,x3,⋯,xn>Y=<y1,y2,y3,⋯,ym>
求X和Y的最长公共子序列。
实例:
X:A B C B D A B
Y:B D C A B A
最长公共子序列为:BCBA,长度为4
问题分析
以X=<x1,x2,⋯ ,xn>,Y=<y1,y2,⋯ ,ym>,Z=<z1,z2,⋯ ,zkX=<x_1,x_2,\cdots,x_n>,Y=<y_1,y_2,\cdots,y_m>,Z=<z_1,z_2,\cdots,z_kX=<x1,x2,⋯,xn>,Y=<y1,y2,⋯,ym>,Z=<z1,z2,⋯,zk做一般性说明,其中Z表示XY的最长公共子序列,一定有下述条件:
①若xn=ym→zk=xn=ymx_n = y_m \rightarrow z_k=x_n=y_mxn=ym→zk=xn=ym,且Zk−1Z_{k-1}Zk−1是Xn−1,Ym−1X_{n-1},Y_{m-1}Xn−1,Ym−1的LCS。
②若xn≠ym,xn≠zk{x_n} \neq {y_m},x_n\neq z_kxn̸=ym,xn̸=zk,ZZZ是Xn−1X_{n-1}Xn−1与YmY_mYm的LCS。
③若xn≠ym,ym≠zkx_n \ne y_m,y_m\ne z_kxn̸=ym,ym̸=zkZZZ是XnX_{n}Xn与Ym−1Y_{m-1}Ym−1的LCS。
令C[i,j]C[i,j]C[i,j]表示Xi与YjX_i与Y_jXi与Yj的LCS的长度,那么递推表达式可以写成:
C[i,j]={0i=0orj=0C[i−1,j−1]+1i,j>0,xi=yjmax{C[i,j−1],C[i−1,j]}i,j>0,xi≠yj
C[i,j] =
\begin{cases}
0 & i=0 \quad or\quad j=0 \\
C[i-1,j-1]+1 & i,j>0, \quad x_i=y_j \\
max\{C[i,j-1],C[i-1,j]\} & i,j>0,\quad x_i \ne y_j
\end{cases}
C[i,j]=⎩⎪⎨⎪⎧0C[i−1,j−1]+1max{C[i,j−1],C[i−1,j]}i=0orj=0i,j>0,xi=yji,j>0,xi̸=yj
标记函数为:
B[i,j]={↖ifC[i,j]=C[i−1,j−1]+1↑ifC[i,j]=C[i−1,j]←ifC[i,j]=C[i,j−1]
B[i,j]=\begin{cases}
\nwarrow & if \quad C[i,j] = C[i-1,j-1]+1 \\
\uparrow & if \quad C[i,j] = C[i-1,j] \\
\leftarrow & if \quad C[i,j] = C[i,j-1]
\end{cases}
B[i,j]=⎩⎪⎨⎪⎧↖↑←ifC[i,j]=C[i−1,j−1]+1ifC[i,j]=C[i−1,j]ifC[i,j]=C[i,j−1]
算法实现
public class LongestCommonSubsequence {
public static int [][]mem;
public static int [][]s;
public static int [] result; // 记录子串下标
public static int LCS(char []X,char []Y,int n,int m){
for (int i = 0; i <= n; i++) {
mem[i][0] = 0;
s[i][0] = 0;
}
for (int i = 0; i <= m; i++) {
mem[0][i] = 0;
s[0][i] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m ; j++) {
if (X[i-1] == Y[j-1]){
mem[i][j] = mem[i-1][j-1] + 1;
s[i][j] = 1;
}
else {
mem[i][j] = Math.max(mem[i][j-1],mem[i-1][j]);
if (mem[i][j] == mem[i-1][j]){
s[i][j] = 2;
}
else s[i][j] = 3;
}
}
}
return mem[n][m];
}
// 追踪解
public static void trace_solution(int n,int m){
int i = n;
int j = m;
int p = 0;
while (true){
if (i== 0 || j == 0) break;
if (s[i][j] == 1 ){
result[p] = i;
p++;
i--;j--;
}
else if (s[i][j] == 2){
i--;
}
else { //s[i][j] == 3
j--;
}
}
}
public static void print(int [][]array,int n,int m){
for (int i = 0; i < n + 1; i++) {
for (int j = 0; j < m + 1; j++) {
System.out.printf("%d ",array[i][j]);
}
System.out.println();
}
}
public static void main(String[] args) {
char []X = {'A','B','C','B','D','A','B'};
char []Y = {'B','D','C','A','B','A'};
int n = X.length;
int m = Y.length;
// 这里重点理解,相当于多加了第一行 第一列。
mem = new int[n+1][m+1];
// 1 表示 左上箭头 2 表示 上 3 表示 左
s = new int[n+1][m+1];
result = new int[Math.min(n,m)];
int longest = LCS(X,Y,n,m);
System.out.println("备忘录表为:");
print(mem,n,m);
System.out.println("标记函数表为:");
print(s,n,m);
System.out.printf("longest : %d \n",longest);
trace_solution(n,m);
// 输出注意 result 记录的是字符在序列中的下标
for (int k = longest-1; k >=0 ; k--) {
// 还需要再减一 才能跟 X Y序列对齐。
int index = result[k]-1;
System.out.printf("%c ",X[index]);
}
}
}