动态规划之 0-1 字符数量问题
- 前言
本问题来源于LeetCode问题,它隶属于经典的0-1背包选择问题,但本问题的特点是,它有两个两个背包,两个背包的约束条件并不相同,因而无法直接套用0-1背包的求解模板。新问题带来一定的挑战,进而要求我们从新的思维角度出发解决问题,引发DP高维数组尝试。
- 问题描述
题目的操作对象为,字符串数组,字符串内容规定只能是‘0’或‘1’,其它字符不允许包含在字符串内。引用LeetCode 问题描述。
给你一个二进制字符串数组
strs
和两个整数m
和n
。请你找出并返回strs
的最大子集的长度,该子集中 最多 有m
个0
和n
个1
。如果x
的所有元素也是y
的元素,集合x
是集合y
的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
- 递归的问题解决
动态规划问题一般都具有求最大值,最小值或计算满足某条件总数等特点,本题可以归结为求解约束条件下的最大子集数量问题。一般在进行设计动态规划DP数组之前,需要理清过程中变量的数量,如果过程中有两个可变量,那么可能设计DP二维数组就满足题目要求;如果过程中有N个可变量,那么就可能构建N维度数组比较符合题目要求。结合本题目特点,识别过程中有三个可变量需要记录其状态,第一个可变量是子集元素的数量,假定用符号i表示;第二个可变量是‘0’的元素数量,可以选择符号j来表示;第三个可变量为‘1’的元素数量,用符号’k’来表示。通过动态规划的练习发现,每次函数递归或递归退出,对所有变量,其实只能记录某一个状态。这个状态如果用递归记录,那么就需要借用栈的资源;这个状态如果用迭代输出,那么就需要利用for或者while循环进行相应的状态输出。值得一提的是,特定时刻的状态具有唯一性和确定性,这也是理解状态机概念的基本出发点。
基于动态规划的 CRCC解题框架,依照这个流程对问题进行逐步解析和剖析,加深动态规划流程的印象。
a.) 表针最优问题的子结构(Characterize the structure of the optimal solution)
问题需要求解最大的子集,从问题描述上来看,具备最大值的特点,从最朴素的理解出发,如果要求解最大子集,自然而然会定义问题:
f
(
s
e
t
,
m
,
n
)
=
m
a
x
{
f
(
s
e
t
i
,
j
,
k
)
}
+
1
;
i
r
e
p
r
e
s
e
n
t
s
t
h
e
n
u
m
b
e
r
o
f
e
l
e
m
e
n
t
i
n
t
h
e
s
t
r
i
n
g
.
0
<
=
j
<
=
m
0
<
=
k
<
=
n
f(set,m,n)=max\{f(set_i,j,k)\}+1;\\ i\ represents\ the\ number\ of\ element\ in\ the\ string. \\ 0<=j<=m \\ 0<=k<=n
f(set,m,n)=max{f(seti,j,k)}+1;i represents the number of element in the string.0<=j<=m0<=k<=n
这个问题就涉及到最优子结构的问题,对于每次操作,其选择的代价都是1,如果不考虑区分‘0’和’1’,我们就可以构建遍历的二叉树,我们要求解的问题属于此遍历二叉树的子集。
b). 递归定义最优问题的值(Recursively define the value of an optimal solution)
递归定义的核心问题之一是确定递归退出的基本条件,此问题递归退出的以字符串数目为基准,当字符串总数满足上限的条件,此时就需要对0和1的数量进行判断,决定是满足0和1的数量要求,以及是否满足最大字符串数量的基本要求。
求解最优值的定义已经在上一小节中完成,无须赘述。
c). 计算最优解的值(Compute the value of the optimal solution)
利用递归算法,我们对最优问题进行递归计算,具体代码实现,采用C语言实现。实现过程中体现了部分回溯算法思想,定义全局变量times, 当对所有的字符串完成选择后,对其中的0和1的数量进行判断,如果满足要求且times的值比较大,那么就更新max_value的值。
/**
* @file find_max_form.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-03-15
*
* @copyright Copyright (c) 2023
*
*/
#ifndef FIND_MAX_FORM_C
#define FIND_MAX_FORM_C
#include "find_max_form.h"
void find_max_form(int i, int zero, int one, char **strs, int str_size, int m, int n)
{
if(i==str_size)
{
if(zero<=m && one<=n)
{
if(max_value<times)
{
max_value=times;
}
}
}
else
{
times++;
find_max_form(i + 1, zero + num_of_zero(strs[i]), one + (strlen(strs[i]) - num_of_zero(strs[i])),strs,str_size,m,n);
times--;
find_max_form(i + 1, zero - num_of_zero(strs[i]), one -(strlen(strs[i]) - num_of_zero(strs[i])), strs,str_size, m, n);
}
}
int num_of_zero(char *str)
{
int num=0;
while(*str!='\0')
{
if(*str=='0')
{
num++;
}
str++;
}
return num;
}
#endif
d.) 从计算过程中,构建最优解的集合(Construct an optimal solution from the computed information)
- 迭代方法问题解决(Bottom-up 方法)
采用bottom-up的迭代方法解决问题,最直观的理解就是白板上搭积木,很多问题都从白班或者∅着手,然后对不同的选择进行比较分析,DP动态数组的维度取决于递归中可变变量的数量。本问题中存在三个可变变量,那么就需要设计三维DP数组进行状态的跟踪dp[i][j][k]。
迭代方法的代码实现,
/**
* @file find_max_form.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-03-15
*
* @copyright Copyright (c) 2023
*
*/
#ifndef FIND_MAX_FORM_C
#define FIND_MAX_FORM_C
#include "find_max_form.h"
int find_max_form(char **strs, int str_size, int m, int n)
{
int i;
int j;
int k;
int num_one;
int num_zero;
int dp[str_size+1][m+1][n+1];
memset(dp,0,sizeof(dp));
for(i=1;i<=str_size;i++)
{
num_zero=num_of_zero(*(strs+i-1));
num_one=strlen(*(strs+i-1))-num_zero;
for(j=0;j<=m;j++)
{
for(k=0;k<=n;k++)
{
if(j>=num_zero && k>=num_one)
{
dp[i][j][k] = max(dp[i - 1][j][k],dp[i - 1][j - num_zero][k - num_one]+1);
}
else
{
dp[i][j][k]=dp[i-1][j][k];
}
}
}
}
return dp[str_size][m][n];
}
int num_of_zero(char *str)
{
int num=0;
while(*str!='\0')
{
if(*str=='0')
{
num++;
}
str++;
}
return num;
}
int max(int m, int n)
{
return(m>n?m:n);
}
#endif
- 总结
本问题可以理解为两个背包的容量问题,针对同一物品展开,它具有两个属性,同一物品分开放置到两个背包中,而且两个背包的最大容量不一致。
参考资料: