解动态规划最重要的步骤个人觉得是找出描述子问题的数据结构,记住里面存储的是子问题的最终结果,而不是一个中间值。
问题一:硬币找零
假设有几种硬币,如1、3、5,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。
解:找子结构,设sum[N]为找N元所需的最小硬币数,则若最后一个是找的3元,则sum[N - 3]一定是找N-3元钱所需硬币数的最优解,即:
sum[N] = min(sum[N-3],sum[N-1],sum[N-5]) + 1
//为了保证sum下标非负
sum[0] = 0
sum[1] = 1
sum[2] = 2
sum[3] = 1
sum[4] = 2
sum[5] = 1
for i 6 to N:
sum[i] = min(sum[i - 1],sum[i - 3],sum[i - 5]) + 1
上面并没有记录结果如何拿,若要记录,每次记下min(sum[i - 1],sum[i - 3],sum[i - 5])中最小的是1还是3还是5即可。
问题二:矩阵链乘
给定有n个要相乘的矩阵构成的序列(链)A1,A2,A3,…….,An>,要计算乘积A1A2…..An,给其画括号已减少计算乘法的次数。
解:找最优子结构,由于括号是可以任意划分的,并不一定从头开始,所以对于任意一段长度的Ai……..Aj,若有括号(Ai……Ak),(Ak……Aj),那么这2个子序列一定是计算乘法次数最少的(当然Ai…….Ak之间也能继续画括号)。即:
sum[i][j] = min(sum[i][k] + sum[k][j] + Pi-1 * Pk *Pj) (k从i到j)
for i 0 to N:
for j i to N:
for k i to j:
if(min < sum[i][k] + sum[k][j] + Pi-1 * Pk *Pj):
min = sum[i][k] + sum[k][j] + Pi-1 * Pk *Pj
index = k
res[i][j] = min
base[i][j] = index
最后再将res[0][N]打出来即可,并且通过k = base[0][N]将序列分成0 - k 和k - N然后不断逆归得到括号的位置。
问题三:捡苹果
平面上有N*M个格子,每个格子中放着一定数量的苹果。从左上角的格子开始, 每一步只能向下走或是向右走,每次走到一个格子就把格子里的苹果收集起来, 这样一直走到右下角,问最多能收集到多少个苹果。
解:找到最优子结构,由于只能下或者右,那么如果走到位置(i,j),那么其一定是要么从上面走来要么从左边走来,而且是走捡到苹果最大的,即
res[i][j] = max(res[i - 1][j],res[i][j - 1]) + P[i][j]
for i 0 to N:
for j to M:
res[i][j] = max(res[i - 1][j],res[i][j - 1]) + P[i][j]
最终将res表中最大的打出即可,若要回溯得到路径,则每次记录max(res[i - 1][j],res[i][j - 1])中最大的那个,比如1:向上 2:向左
问题四:LCS
对于序列S和T,求它们的最长公共子序列。例如X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A}则它们的lcs是{B,C,B,A}和{B,D,A,B}。求出一个即可
解:最优子结构,证明在前缀中也有最长公共子串,设LCS[i][j]为最长公共子串的长度:
1、当stringa[i] = stringb[j]时,则LCS[i][j] = LCS[i - 1][j - 1] + 1,
2、当stringa[i] != stringb[j]时,则LCS[i][j] = max(LCS[i - 1][j],LCS[i][j - 1])
再用LCSpath记录每次选的方向是0、1、2即可回溯找到最长的子串
#这里的i和j都是前缀长度,故可以取到len
for i in range(0,len(stringa) + 1):
for j in range(0,len(stringb) + 1):
#初始化,有0的时候长度全为0
if (i == 0) | (j == 0):
LSCN[i][j] = 0
LCS[i][j] = -1
continue
#这里的index才是数组下标
IndexA = i - 1
IndexB = j - 1
#当相等的时候即+1
if stringa[IndexA] == stringb[IndexB]:
LSCN[i][j] = LSCN[i - 1][j - 1] + 1
LCS[i][j] = 1
#当不等的时候取目前公共子串较长的
elif (LSCN[i - 1][j] > LSCN[i][j - 1]):
LSCN[i][j] = LSCN[i - 1][j]
LCS[i][j] = 0
else:
LSCN[i][j] = LSCN[i][j - 1]
LCS[i][j] = 2
问题五:0-1背包
有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和
解:子结构为:
1、如果当前拿上这个物品是最优的GetiPath_Capacity[i][Cap],则在Cap = Cap - w[i]这个时候GetiPath_Capacity[i - 1][Cap - w[i]]是最优的。
2、如果最优解不包括第i个物品,那么GetiPath_Capacity[i - 1][Cap]就是最优的
状态转移方程即为
GetiPath_Capacity[i][Cap] = Max(GetiPath_Capacity[i - 1][Cap],GetiPath_Capacity[i][Cap - w[i]] + v[i])
GetiPath_Capacity[i - 1][Cap]:不拿第i个物体在Cap容量下的更好
GetiPath_Capacity[i - 1][Cap - w[i]] + v[i]:拿了第i个物体在Cap容量下最好,可以看到这里利用了前面的子结构.
#Capacity + 1是因为Cap是容量 不仅仅是数组下标,数组下标不含10
for Cap in range(0,Capacity + 1):
for i in range(0,N):
#保证Cap - w[i]大于0,这样才能装下东西
if Cap - w[i] < 0:
GetiPath_Capacity[i][Cap] = 0
continue;
if i == 0:
GetiPath_Capacity[i][Cap] = v[i]
isGetiPath[i][Cap] = 1
if GetiPath_Capacity[i - 1][Cap] > GetiPath_Capacity[i - 1][Cap - w[i]] + v[i]:
GetiPath_Capacity[i][Cap] = GetiPath_Capacity[i - 1][Cap]
else:
GetiPath_Capacity[i][Cap] = GetiPath_Capacity[i - 1][Cap - w[i]] + v[i]
isGetiPath[i][Cap] = 1
问题六:字符串距离
对于不同的字符串,判断其相似度。
定义了一套操作方法来把两个不相同的字符串变得相同,具体的操作方法为:
1.修改一个字符(如把“a”替换为“b”)
2.增加一个字符(如把“abdd”变为“aebdd”)
3.删除一个字符(如把“travelling”变为“traveling”)
定义:把这个操作所需要的最少次数定义为两个字符串的距离,而相似度等于“距离+1”的倒数
解:LCS为其特殊情况,对于string a的i前缀和string b的j前缀:
1、stringa[i] == stringb[j],则Dis[i][j] = Dis[i - 1][j - 1]
2、stringa[i] != stringb[j],则Dis[i][j] = min(Dis[i - 1][j],Dis[i][j - 1]) + 1,因为不论是添加、删除还是改变操作,操作的值都是1,如果定义操作不同距离不同,则方程应该为:
Dis[i][j] = min(Dis[i - 1][j] + ActionCost[k]) k = 0………M
#1:a string 进行变更 2:b string进行变更 3: 都不变
for i in range(0,la + 1):
for j in range(0,lb + 1):
if i == 0:
Dis[i][j] = j
continue
if j == 0:
Dis[i][j] = i
continue
IndexA = i - 1
IndexB = j - 1
if stringa[IndexA] == stringb[IndexB]:
Dis[i][j] = Dis[i - 1][j - 1]
Action[i][j] = 3
elif Dis[i - 1][j] > Dis[i][j - 1]:
Dis[i][j] = Dis[i][j - 1] + 1
Action[i][j] = 2
else:
Dis[i][j] = Dis[i - 1][j] + 1
Action[i][j] = 1
Action记录如何改变的,最后可以打出变化序列,1:a string 进行变更 2:b string进行变更 3: 都不变
i = lengtha
j = lengthb
while (i != 0) & (j != 0):
c = Action[i][j]
print(c)
if c == 1:
i -= 1
elif c == 2:
j -= 1
elif c == 3:
i -= 1
j -= 1
else:
i = -1
问题七:二插搜索树个数问题
给一个数n,问能建多少种不同的二插搜索树,eg:
解:假设sum[n]保存数字为n时的数目,那么可以看到当n = n+1时,加上了n+1这个最大的数,这个数可以:
1、当树根 = sum[n - 1] (直接放root,下面都不变)
2、当树叶 = sum[n - 1] (直接插入n-1时刻的每一个树,由于n最大,肯定是成叶子节点)
3、当中间节点 = sum[i] * sum[n - i - 1](由于是搜索树,所以放中间的时候其上面和下面部分都一定是连续的,即上面是1 to i,下面是i + 1 to n - 1)
公式: sum[n] = sum[n - 1] * 2 + sum[i] * sum[n - i - 1] (i 1 to n - 2)
注意i不能为n-1,否则n就成叶子了
for(int j = 1; j <= n; j++)
{
if(j == 1)
{
sum[j] = 1;
continue;
}
int midSum = 0;
for(int i = 1; i < j - 1; i++)
midSum += sum[i] * sum[j - i - 1];
sum[j] = midSum + sum[j - 1] * 2;
}
Ps:这里还有一个打印上面所有BST的题目,代码如下,主要复习下树的遍历以及指针和链表的使用:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
void TreeTraversalCopy(TreeNode *p, TreeNode **nowp)
{
if (p == NULL)
{
*nowp = NULL;
return;
}
*nowp = (TreeNode*)malloc(sizeof(TreeNode));
(*nowp)->val = p->val;
TreeTraversalCopy(p->left,&((*nowp)->left));
TreeTraversalCopy(p->right, &((*nowp)->right));
}
void TreeTraversalChange(TreeNode **p, int i)
{
if (*p == NULL)
return;
(*p)->val = (*p)->val + i - 1;
TreeTraversalChange(&((*p)->left), i);
TreeTraversalChange(&((*p)->right), i);
}
TreeNode * FindMostRight(TreeNode * p, int val)
{
TreeNode * temp = p;
while (p->val != val)
p = p->right;
return p;
}
class Solution {
public:
vector<TreeNode*> generateTrees(int n) {
int sum[1000];
vector<vector<TreeNode*>> pTreeSet;
vector<TreeNode*> temp;
pTreeSet.push_back(temp);
for (int j = 1; j <= n; j++)
{
vector<TreeNode*> temp;
pTreeSet.push_back(temp);
if (j == 1)
{
TreeNode *p = (TreeNode*)malloc(sizeof(TreeNode));
sum[j] = 1;
p->val = j;
p->left = NULL;
p->right = NULL;
pTreeSet[j].push_back(p);
continue;
}
int midSum = 0;
//n在中间
for (int i = 1; i < j - 1; i++)
{
for (int k = 0; k < sum[i]; k++)
for (int m = 0; m < sum[j - i - 1]; m++)
{
TreeNode *p = (TreeNode*)malloc(sizeof(TreeNode));
p->val = j;
p->left = NULL;
p->right = NULL;
TreeNode *pUpRoot = NULL, *pDownRoot = NULL;
TreeTraversalCopy(pTreeSet[i][k], &pUpRoot);
TreeTraversalCopy(pTreeSet[j - i - 1][m], &pDownRoot);
TreeTraversalChange(&pDownRoot, i + 1);
TreeNode *iter = FindMostRight(pUpRoot, i);
iter->right = p;
p->left = pDownRoot;
pTreeSet[j].push_back(pUpRoot);
}
midSum += sum[i] * sum[j - i - 1];
}
for (int i = 0; i < sum[j - 1]; i++)
{
//n在根节点,子树不用复制
TreeNode *pRoot = (TreeNode*)malloc(sizeof(TreeNode));
pRoot->val = j;
pRoot->left = NULL;
pRoot->right = NULL;
(*pRoot).left = pTreeSet[j - 1][i];
pTreeSet[j].push_back(pRoot);
//n在叶子,需要将树重新复制一遍
TreeNode *pleafRoot = NULL;
TreeNode *pLeaf = (TreeNode*)malloc(sizeof(TreeNode));
pLeaf->val = j;
pLeaf->left = NULL;
pLeaf->right = NULL;
TreeTraversalCopy(pTreeSet[j - 1][i], &pleafRoot);
TreeNode *iter = FindMostRight(pleafRoot, j - 1);
iter->right = pLeaf;
pTreeSet[j].push_back(pleafRoot);
}
sum[j] = midSum + sum[j - 1] * 2;
}
return pTreeSet[n];
}
};
问题八:丑数字问题
丑数字:即质因数只有2、3、5的数,1也算,输入n,请给出第n个丑数字。
eg:n = 10 丑数字为:1, 2, 3, 4, 5, 6, 8, 9, 10, 12
解:直接一个个判断太慢,由于这样的数字为少数,所以我们找出他们再排序,可以发现这些数字都是由2,3,5乘起来的,于是我们可以建立3个list L2、L3、L5,分别表示由2,3,5所乘起来的数。注意:这里的子问题是:
Dp[i] = min(Dp[L2] * 2,Dp[L3] * 3,Dp[L5] * 5)
UglyNumSet = [1]
count = 1
L2 = L3 = L5 = 0
while(count < n):
PredictL2 = UglyNumSet[L2] * 2
PredictL3 = UglyNumSet[L3] * 3
PredictL5 = UglyNumSet[L5] * 5
if PredictL2 < PredictL3:
if PredictL2 < PredictL5:
if PredictL2 == UglyNumSet[count - 1]:
L2 += 1
continue
L2 += 1
UglyNumSet.append(PredictL2)
count += 1
continue
else:
if PredictL5 == UglyNumSet[count - 1]:
L5 += 1
continue
L5 += 1
UglyNumSet.append(PredictL5)
count += 1
continue
if PredictL3 < PredictL5:
if PredictL3 == UglyNumSet[count - 1]:
L3 += 1
continue
L3 += 1
UglyNumSet.append(PredictL3)
count += 1
else:
if PredictL5 == UglyNumSet[count - 1]:
L5 += 1
continue
L5 += 1
UglyNumSet.append(PredictL5)
count += 1
return UglyNumSet[n - 1]