[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jVqrH7LL-1638272460553)(C:\Users\26969\AppData\Roaming\Typora\typora-user-images\image-20211002210519339.png)]
该类子问题与位置有关,通常固定一端,通过移动另一端边界(1维或2维)构造子问题递推关系
题目描述:给定由n个整数(可能为负整数)组成的序列a1,a2,…,an,求该序列子段和的最大值。当所有整数均为负整数时定义其最大子段和为0。依此定义,所求的最优值为:
例,序列{-2,11,-4,13,-5,-2}的最大子段和为20
int MaxSum1(int n,int* a,int& besti,int& bestj){
int sum=0;
for(int i=1;i<=n;i++) //注意:a[0]不用
for(int j=i;j<=n;j++)
{
int thissum=0;
for(int k=i;k<=j;k++)
thissum+=a[k];
if(thissum>sum)
{
sum=thissum;
besti=i;
bestj=j;
}
}
return sum;
}
T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3)
注意到 ∑ k = i j a k = a j + ∑ k = i j − 1 a k \sum_{k=i}^{j}a_k=a_j+\sum_{k=i}^{j-1}a_k ∑k=ijak=aj+∑k=ij−1ak,则可以将算法中的最后一个for循环去掉。
int MaxSum2(int n,int *a,int &besti,int &bestj){
int sum=0;
for(int i=1;i<=n;i++)
{
int thissum=0;
for(int j=i;j<=n;j++)
{
thissum+=a[j];
if(thissum>sum)
{
sum=thissum;
besti=i;
bestj=j;
}
}
}
return sum;
}
T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2)
将序列a[1:n]分为长度相等的两段a[1:n/2]和a[n/2+1:n],分别求出这两段的最大子段和,则a[1:n]的最大子段和有三种情形:
1️⃣ 与a[1:n/2]的最大子段和相同;
2️⃣ 与a[n/2+1:n]的最大子段和相同;
/**
* 使用分治法解决
* @param a
* @param left
* @param right
* @return
*/
public static int soveByDivide(int[] a, int left, int right) {
int sum = 0;
if (left == right)
sum = a[left] > 0 ? a[left] : 0;//判断正负
else {
int middle = (left + right) / 2;
int leftSum = soveByDivide(a, left, middle);
int rightSum = soveByDivide(a, middle + 1, right);//分治
int s1 = 0;
int lefts = 0;
for (int i = middle; i >= left; i--) {
lefts += a[i];
if (lefts > s1)
s1 = lefts;
}
int s2 = 0;
int rights = 0;
for (int j = middle + 1; j <= right; j++) {
rights += a[j];
if (rights > s2)
s2 = rights;
}
sum = s1 + s2;
if (sum < leftSum)
sum = leftSum;
if (sum < rightSum)
sum = rightSum;
}
return sum;
}
由b[j]的定义易知,当b[j-1]>0时,b[j]=b[j-1]+a[j],否则b[j]=a[j],故
/**
* 动态规划
* @param a
* @return
*/
public static int solveByDP(int[]a){
int sum=0,b=0;
for (int i = 0; i <a.length ; i++) {
if (b>0)
b+=a[i];
else
b=a[i];
if (b>sum)
sum=b;
}
return sum;
}
最大子段和问题可以推广到高维的情形。
给定一个m行n列的整数矩阵A,试求矩阵A的一个子矩阵,使其各元素之和为最大。
/**
* 最大子矩阵和
* @param m
* @param n
* @param a
* @return
*/
public static int MaxSum2(int m,int n,int[][]a){
int sum=0;
int[]b=new int[n];
for(int i=0;i<m;i++){
//从第i行
for(int k=0;k<n;k++) //初始化数组b
b[k]=0;
for(int j=i;j<m;j++){
//到第j行
for(int k=0;k<n;k++)
b[k]+=a[j][k];//按列取值
int max=solveByDP(b);
if(max>sum)
sum=max;
}
}
return sum;
}
给定由n个整数(可能为负数)组成的序列{a1,a2,…,a3},以及一个正整数m,要求确定序列{a1,a2,…,a3}的m个不相交子段,使这m个子段的总和达到最大。
设b(i,j)表示数组a的前j项中i个子段和的最大值,且第i个子段含a[j](1≤i ≤ m,i ≤j ≤n),则计算b(i,j)的递归式为
初始时
b(0,j)=0, (1≤j ≤n)
b(i,0)=0, (1≤i ≤m)
int MaxSum(int m,int n,int *a)
{
if(n<m||m<1)
return 0;
int **b=new int *[m+1]; //定义二维数组b
for(int i=0; i<=m; i++)
b[i]=new int[n+1];
for(int i=0; i<=m; i++) //初始值
b[i][0]=0;
for(int j=1; j<=n; j++)
b[0][j]=0;
for(int i=1; i<=m; i++) //1≤i ≤m
for(int j=i; j<n-m+i; j++) //j≥i, t
if(j>i)
{
b[i][j]=b[i][j-1]+a[j];
for(int k=i-1; k<j; k++) //i-1≤t
if(b[i][j]<b[i-1][k]+a[j])
b[i][j]=b[i-1][k]+a[j];
}
else //j=i, 每个数都是一个子段
b[i][j]=b[i-1][j-1]+a[j];
int sum=0;
for(int j=m; j<=n; j++)
if(sum<b[m][j])
sum=b[m][j];
return sum;
}
计算机中常用像素点灰度值序列{ p 1 , p 2 , . . . , p n p_1,p_2,...,p_n p1,p2,...,pn}表示图像, p i p_i pi表示像素点i的灰度值。灰度值的范围常为0~255,需要用8位来表示。
图像的变位压缩存储格式将所给的像素点序列{ p 1 , p 2 , . . . , p n p_1,p_2,...,p_n p1,p2,...,pn}分割成m个连续段{ S 1 , S 2 , . . . , S m S_1,S_2,...,S_m S1,S2,...,Sm}。第i个像素段Si中有l[i]个像素,且该段中每个像素都只用b[i]位表示。需用3位表示b[i],如果限制1≤l[i]≤255,则需要用8位表示l[i],因此第i个像素段所需的存储空间为l[i]*b[i]+11。——即一段中最多有255个像素,用8位二进制表示
问题描述:确定像素序列{p1,p2,…,pn}的一个最优分段,使得依此分段所需的存储空间最小。其中0≤pi ≤255,1 ≤i ≤n,每个分段的长度不超过255位
设l[i],b[i],1≤i ≤m是{ p 1 , p 2 , … , p n p_1,p_2,…,p_n p1,p2,…,pn}的最优分段。显而易见,l[1],b[1]是{ p 1 , p 2 , … , p l [ 1 ] p_1,p_2,…,p_{l[1]} p1,p2,…,pl[1]}的最优分段,且l[i],b[i], 2≤i ≤m是
{ p l [ 1 ] + 1 , … , p n p_{l[1]+1},…,pn pl[1]+1,…,pn}的最优分段。即图象压缩问题满足最优子结构性质。
设s[i],1≤i≤n,是像素序列{ p 1 , p 2 , … , p i p_1,p_2,…,p_i p1,p2,…,pi}的最优分段所需的存储位数。由最优子结构性质易知:
举例:
算法用l[i],b[i]记录了最优分段所需的信息。
最优分段的最后一段的段长和像素位数分别存储于l[n]和b[n]中,其前一段的段长度和像素位数存储于l[n-l[n]]和b[n-l[n]]中。依此类推,可在O(n)时间内构造出相应的最优解
/**
* 计算十进制数i所需的二进制位数
*
* @param i
* @return
*/
static int length(int i) {
int k = 1;
i = i / 2;
while (i > 0) {
k++;
i = i / 2;
}
return k;
}
/**
* @param n
* @param l [p1:pi]的最优分段中最后1个分段的像素个数
* @param p p[p1:pn],像素点灰度值序列
* @param s 像素序列[p1:pi]的最优分段所需的存储位数
* @param b 像素p[i]所需的存储位数
*/
public static void Compress(int n, int[] p, int[] s, int[] l, int[] b) {
int Lmax = 255;//每个分段的长度不超过255位
int header = 11;//分段段头所需的位数,表示一个段的附加信息
s[0] = 0;
for (int i = 1; i <= n; i++) //[p1:pi]
{
b[i] = length(p[i]);
int bmax = b[i];
s[i] = s[i - 1] + bmax; //k=1
l[i] = 1;
for (int j = 2; j <= i && j <= Lmax; j++) //最后的1个分段中有j个像素
{
if (bmax < b[i - j + 1])
bmax = b[i - j + 1];//这一段中的最大位数
if (s[i] > s[i - j] + j * bmax) {
//找到更好的分段
s[i] = s[i - j] + j * bmax;
l[i] = j;
}
}
s[i] += header;//加上额外开销
}
}
public static int Traceback(int n, int i, int[] s, int[] l) {
if (n == 0)
return i;
i = Traceback(n - l[n], i, s, l);
s[i++] = n - l[n];// 重新为s[]数组赋值,用来存储分段位置
return i;
}
static void Output(int s[], int l[], int b[], int n) {
System.out.println("The optimal value is " + s[n]);
int m = 0;
m=Traceback(n, m, s, l); //m:分段数
s[m] = n; //m个分段像素的累积和,Traceback算到m-1个
System.out.println("Decompose into " + m + " segments");
for (int j = 1; j <= m; j++) {
l[j] = l[s[j]]; //计算第j个分段像素个数: l[j]
b[j] = b[s[j]]; //计算第j个分段所需的存储位数: b[j]
}
for (int j = 1; j <= m; j++)
System.out.println(l[j] + " " + b[j]);
}
public static void main(String[] args) {
int p[] = {
0,10,12,15,255,2,1};//第一位不算
int N=p.length;
int s[] = new int[N];
int l[] = new int[N];
int b[] = new int[N];
Compress(N-1, p, s, l, b);
Output(s, l, b, N-1);
}
}
由于算法compress中对k的循环次数不超这255,故对每一个确定的i,可在时间O(1)内完成的计算。因此整个算法所需的计算时间为O(n)。
若给定序列 X = { x 1 , x 2 , … , x m } X=\{x_1,x_2,…,x_m\} X={ x1,x2,…,xm},则另一序列 Z = { z 1 , z 2 , … , z k } Z=\{z_1,z_2,…,z_k\} Z={ z1,z2,…,zk},是X的子序列是指存在一个严格递增下标序列 { i 1 , i 2 , … , i k } \{i_1,i_2,…,i_k\} { i1,i2,…,ik}使得对于所有j=1,2,…,k有: z j = x i j z_j=x_{ij} zj=xij。例如,序列Z={B, C, D, B}是序列X={A, B, C , B, D, A, B}的子序列,相应的递增下标序列为{2, 3, 5, 7}。给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。例:X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A},则序列{B,C,A}是X和Y的一个公共子序列。
设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk} ,则
⑴若xm=yn,则zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列。
⑵若xm≠yn且zk≠xm,则Z是Xm-1和Y的最长公共子序列。
⑶若xm≠yn且zk≠yn,则Z是X和Yn-1的最长公共子序列。
由此可见,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。
由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。用 c [ i ] [ j ] c[i][j] c[i][j]记录序列的最长公共子序列的长度。其中, X i = { x 1 , x 2 , … , x i } ; Y j = { y 1 , y 2 , … , y j } Xi=\{x1,x2,…,xi\};Yj=\{y1,y2,…,yj\} Xi={ x1,x2,…,xi};Yj={ y1,y2,…,yj}。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列。故此时 C [ i ] [ j ] C[i][j] C[i][j]=0。其它情况下,由最优子结构性质可建立递归关系如下:
由于在所考虑的子问题空间中,总共有θ(mn)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。
输入:x,y (序列数组)
输出:
c [ i ] [ j ] c[i][j] c[i][j],存储x[1:i]和y[1:j]的最长公共子序列的长度;
b [ i ] [ j ] b[i][j] b[i][j],记录上面 c [ i ] [ j ] c[i][j] c[i][j]的值是由哪个子问题的解得到的。
/**
* 计算最长公共子序列
* @param x 序列数组
* @param y 序列数组
* @param c 存储x[1:i]和y[1:j]的最长公共子序列的长度
* @param b 记录上面c[i][j]的值是由哪个子问题的解得到的
*/
public static void LCSLength(char[] x, char[] y, int[][] c, int[][] b) {
int m = x.length-1;
int n = y.length-1;
for (int i = 1; i <= m; i++) {
c[i][0] = 0;
}
for (int i = 1; i <= n; i++) {
c[0][i] = 0;
}//第一个条件
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (x[i] == y[j]) {
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j] = 1;
//表示Xi和Yi的最长公共子序列是由Xi-1和Yi-1的最长公共子序列在尾部加上xi所得到的。
} else if (c[i - 1][j] >= c[i][j - 1]) {
c[i][j] = c[i - 1][j];
b[i][j] = 2;
//表示Xi和Yi的最长公共子序列与Xi-1和Yi的最长公共子序列相同
} else {
c[i][j] = c[i][j - 1];
b[i][j] = 3;
//表示Xi和Yi的最长公共子序列与Xi和Yi-1的最长公共子序列相同
}
}
}
}
算法耗时O(mn)
从 b [ m ] [ n ] b[m][n] b[m][n] 开始,依其值在数组b中搜索。
b [ i ] [ j ] b[i][j] b[i][j]的值为:
1,表示Xi和Yi的最长公共子序列是由Xi-1和Yi-1的最长公共子序列在尾部加上xi所得到的。
2,表示Xi和Yi的最长公共子序列与Xi-1和Yi的最长公共子序列相同。
3, 表示Xi和Yi的最长公共子序列与Xi和Yi-1的最长公共子序列相同。
public static void LCS(int m, int n, char[] x, int[][] b) {
if (m == 0 || n == 0) {
return;
}
if (b[m][n] == 1) {
LCS(m - 1, n - 1, x, b);
System.out.print(x[m]);
} else if (b[m][n] == 2) {
LCS(m - 1, n, x, b);
} else {
LCS(m, n - 1, x, b);
}
}
public static void main(String[] args) {
char[] x = "*abdscde".toCharArray();
char[] y = "*bcde".toCharArray();
int m = x.length;
int n = y.length;
int[][] c = new int[m][n];
int[][] b = new int[m][n];
LCSLength(x, y, c, b);
System.out.println(c[m - 1][n - 1]);
LCS(m - 1, n - 1, x, b);
}
运行结果
4
bcde
在算法lcsLength和lcs中,可进一步将数组b省去。事实上,数组元素 c [ i ] [ j ] c[i][j] c[i][j]的值仅由 c [ i − 1 ] [ j − 1 ] , c [ i − 1 ] [ j ] 和 c [ i ] [ j − 1 ] c[i-1][j-1],c[i-1][j]和c[i][j-1] c[i−1][j−1],c[i−1][j]和c[i][j−1]这3个数组元素的值所确定。对于给定的数组元素 c [ i ] [ j ] c[i][j] c[i][j],可以不借助于数组b而仅借助于c本身在O(1)时间内确定 c [ i ] [ j ] c[i][j] c[i][j]的值是由 c [ i − 1 ] [ j − 1 ] , c [ i − 1 ] [ j ] 和 c [ i ] [ j − 1 ] c[i-1][j-1],c[i-1][j]和c[i][j-1] c[i−1][j−1],c[i−1][j]和c[i][j−1]中哪一个值所确定的。
如果只需要计算最长公共子序列的长度,则算法的空间需求可大大减少。事实上,在计算 c [ i ] [ j ] c[i][j] c[i][j]时,只用到数组c的第i行和第i-1行。因此,用2行的数组空间就可以计算出最长公共子序列的长度。进一步的分析还可将空间需求减至O(min(m,n))。
在一块电路板的上、下2端分别有n个接线柱。根据电路设计,要求用导线(i,π(i))将上端接线柱与下端接线柱相连,其中π(i)是{1,2,…,n}的一个排列。导线(i,π(i))称为该电路板上的第i条连线。对于任何1≤i
电路布线问题要确定将哪些连线安排在第一层上,使得该层上有尽可能多的连线。换句话说,该问题要求确定导线集Nets={(i,π(i)),1≤i≤n}的最大不相交子集。
最优值即为Size(n,n)
void MNS(int C[],int n,int **size){
//C[i],即π[i]
//size[i][j],N(i,j)的最大不相交子集中连线的数目
for(int j=0; j<C[1]; j++) //i=1,j<π(1)
size[1][j]=0;
for(int j=C[1]; j<=n; j++) //i=1,j>=π(1)
size[1][j]=1;
for(int i=2; i<n; i++) //1
{
for(int j=0; j<C[i]; j++) //j<π(i)
size[i][j]=size[i-1][j];
for(int j=C[i]; j<=n; j++) //j>=π(i)
size[i][j]=max(size[i-1][j],size[i-1][C[i]-1]+1);
}
size[n][n]=max(size[n-1][n],size[n-1][C[n]-1]+1); //i=n,j=n
}
void Traceback(int C[],int **size,int n,int Net[],int &m)
{
//Net[0:m-1]存储MNS(n,n)中的m条连线
int j=n;
m=0;
for(int i=n; i>1; i--)
if(size[i][j]!=size[i-1][j]) //第i条连线∈MNS(n,n)
{
Net[m++]=i;
j=C[i]-1; //π[i]
}
if(j>=C[1]) //i=1
Net[m++]=1;
}
重要:背下来
给定n种物品和一背包。物品i的重量是 w i w_i wi,其价值为 v i v_i vi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
0-1背包问题是一个特殊的整数规划问题
设(y1,y2,…,yn)是所给问题的一个最优解,则(y2,y3,…,yn)是下面相应子问题的的一个最优解:
若不然,设(z2,z3,…,zn)是上述子问题的一个最优解。
这说明(y1,z2,…,zn)是所给问题的一个更优解,从而与(y1,y2,…,yn)是所给问题的最优解相矛盾。
设所给0-1背包问题的子问题的最优值为m(i,j),即m(i,j)是背包容量为j,可选择物品为i,i+1,…,n时0-1背包问题的最优值。
由0-1背包问题的最优子结构性质,可以建立计算m(i,j)的递归式如下。
public class KnapsackProblem {
//0-1背包问题
/**
*
* @param v v[1:n],物品i的价值
* @param w w[1:n],物品i的重量
* @param c 背包容量
* @param n
* @param m m[i][j],背包容量为j,可选物品为[i:n]时,0-1背包问题的最优值
*/
public static void Knapsack(int[]v,int[]w,int c,int n,int[][]m){
int jMax = Math.min(w[n]-1,c);
for (int j = 0; j <=jMax; j++) {
m[n][j]=0;//j<=c&&j
}
for (int j = w[n]; j <=c; j++) {
m[n][j]=v[n];//w[n]<=j<=c,物品n可以放入背包
}//画边界,从后往前看
for (int i = n-1; i >1 ; i--) {
jMax = Math.min(w[i]-1,c);
for (int j = 0; j <=jMax; j++) {
m[i][j]=m[i+1][j];//物品i无法放入背包
}
for (int j = w[i]; j <=c ; j++) {
//物品i可放入背包
m[i][j]=Math.max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
}
m[1][c]=m[2][c];
if (c>=w[1]){
m[1][c]=Math.max(m[1][c],m[2][c-w[1]]+v[1]);
}
}
/**
* 求解
* @param m
* @param w
* @param c
* @param n
* @param x 具体的解
*/
public static void TraceBack(int[][]m, int[]w,int c,int n,int[]x){
for(int i=1;i<n;i++)
if(m[i][c]==m[i+1][c])
x[i]=0;
else
{
x[i]=1;
c-=w[i];
}
x[n]=(m[n][c]>0)?1:0;
}
public static void main(String[] args) {
int[]v={
0,1,13,8,4,5,6,7};
int[]w={
0,2,3,1,4,1,5,1};
int c = 10;
int n = v.length;
int[] x = new int[n];
int[][]m=new int[n][c+1];
Knapsack(v,w,c,n-1,m);
TraceBack(m,w,c,n-1,x);
for (int i = 1; i < n; i++) {
System.out.println(x[i]+" ");
}
}
}
Knapsack:O(nc)
Traceback:O(n)