动态规划,核心思想就是一个递推公式以及边界值,数学上叫归纳法。
一维动态规划典型如斐波那契数列,f[i]=f[i-1]+f[i-2];
dp[i]=dp[i-1]+dp[i-2]; 边界值f[1]=1,f[2]=1,即dp[1]=1,dp[2]=1已知。
二维动态规划典型如二维整数数组从左上角到右下角,只能向右或向下运动时的最小、最大路径和:
dp[i-1][j-1] dp[i-1][j]
dp[i][j-1] dp[i][j]
最小路径和:dp[i][j]=min(dp[i-1][j],dp[i][j-1])+arr[i][j]; 最大路径和:dp[i][j]=max(dp[i-1][j],dp[i][j-1])+arr[i][j];
这种直接给出"二维矩阵"、"某一单元格与其他单元格之间递推关系"的近似放水的题,简直是送分。
但是,对一些比较隐晦的题,就要往动规上套了:
参考自"六大算法之三:动态规划https://blog.csdn.net/zw6161080123/article/details/80639932"
1、数组最大不连续递增子序列的长度
arr[] = {3,1,4,1,5,9,2,6,5}的最长不连续递增子序列长度为4。即为:1,4,5,9
子序列,子字符串,我最初试图把这个题当作二维动态规划,dp[i][j]表示下标闭区间[i:j]的子序列的最大不连续递增子序列的长度,然后位于135度对角线上元素dp[k][k]都是1,然后:
dp[i][j-1] dp[i][j]
dp[i+1][j-1] dp[i+1][j]
就拿dp[i][j-1]增加一个arr[j]如何得到dp[i][j]来看,由于[i,j-1]子序列内可能不止一条不连续递增子序列:
在上面这种情况下,必须知道dp[i][j-1]内所有不连续递增子序列的构成,并用arr[j]逐一对比这些不连续递增子序列的尾元素,如果大于,说明能增长。在得到所有可能的长度后取最大长度就是dp[i][j]。这需要dp[i][j-1]维护其所有连续递增子序列,因为即使是较短的子序列,在将来增加新元素后也可能反超成最长的那个子序列。此时已经违背动态规划的思想了,果断中止幻想!
还是看了别人的文章才意识到,这应该要转化为一维动态规划:
dp[i]表示下标[0:i]闭区间内子序列的最大不连续递增子序列的长度,首先边界值dp[0]=1,一个元素构成长度为1的最长不连续递增子序列。对dp[1]:当arr[0]<arr[1]时dp[1]=dp[0]+1=2,否则dp[1]=dp[0]=1。对dp[0]到dp[i],dp[0]到dp[i]至少是1,如果arr是一个单调递减序列,那么dp[0]到dp[i]都是1;
对dp[i-1]到dp[i],新增了一个arr[i],此时需要对dp[i-1]内的每个元素arr[0]到arr[i-1]与arr[i]进行比较,
。。。
2、股票问题,只允许一次交易,找出买入卖出价格点
//只要按时间顺序找到最大价差,因为只允许一次交易嘛
float maxEarn(vector<float> price){
int i(0),j(price.size()-1);
float low(2^31),high(-2^31);
while(i<j){
if(arr[i]<low) low=arr[i++];
if(arr[j]>high) high=arr[j--];
}
return high-low;
}
3、股票问题,不限交易次数
3.1 技巧解法:
/*
股票买卖,给定一个数组,它的第i个元素是一支给定股票第i天的价格,设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。例如[7,1,5,3,6,4]输出7,也即-1+5-3+6=7
思路:状态机决定当前任务是要买还是要卖,买的信号是股价下一步要涨,卖的信号是股价下一步要跌,平价不作为,到数组尾、并且当前要卖时一定要卖出,力求减少损失。到数组尾、并且当前要买时就不买了,减少损失
*/
设置买还是卖的状态机,要买并且将来要涨,那当下必须买;要卖并且将来要跌,那当下必须卖;要买但将来休市,那就不买减少支出;要卖但将来休市,那当下必须卖以回笼资金。局部最优能形成整体最优。
#include<iostream>
#include<vector>
using namespace std;
float maxEarn(const vector<float>& vprice){
bool bbuy(true);
float money(0);
for(int i(0);i<vprice.size();i++){
if(bbuy){
if(i+1<vprice.size() && vprice[i+1]>vprice[i]){
money-=vprice[i]; //要买并且将来要涨,买
bbuy=false;
}else if(i==vprice.size()-1){ //要买,但没卖的机会了,就不买
}
}else{
if(i+1<vprice.size() && vprice[i+1]<vprice[i]){
money+=vprice[i]; //要卖并且将来要跌,卖
bbuy=true;
}else if(i==vprice.size()-1){ //要卖,并且明天休市,必须卖掉回笼资金
money+=vprice[i];
bbuy=true;
}
//要卖并且将来还涨,那现在不着急卖,而是等到将来涨势出现跌时才卖,也就是对{1买,3,6,5}
//这样连续涨,只在6时刻一次卖出,而不用{1买,3卖并3买,6卖,5不作为}在涨势中间时刻卖掉
//旧的买入新的
}
}
return money;
}
int main(int argc,char *argv[]){
int arr[]={1,2,3,4,5};//{7,1,5,3,6,4};
vector<float> price;
for(int i(0);i<5;i++) price.push_back(arr[i]);
std::cout<<maxEarn(price)<<"\n";
}
3.2 暴力穷举,递归
转载自LeetCode 股票问题的一种通用解法https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484509&idx=1&sn=21ace57f19d996d46e82bd7d806a2e3c&source=41#wechat_redirect
思路:穷举嘛,如果第一天买入,那可以在第2天直到休市的任一天n卖出,然后n+1天及之后的第m天又可以买入,然后可以在m+1天直到休市的任一天p卖出。。。,最后一天如果买了就没有机会卖出(buy=len(prices)-1,没有sell)所以不会实际执行买入,也就是不会参与到利润计算中。在当前price[]内穷举第buy天买入第sell天卖出后继续对price[sell+1:]穷举其内第buyA天买入第sellS天卖出。
给出C++代码:
/*
还是上面的prices[]股票买卖问题,用穷举法+递归
举个小例子[1,3,4,5]
*/
float maxEarn2(vector<float> prices,int ibegin){ //递归函数
if(ibegin>=prices.size()-1) return 0.0f; //最后一天如果买入就不能卖出,不能做交易,挣不了钱
float res(0.0f);
float earnSingle(0.0f);
for(int buy(ibegin);buy<prices.size();buy++){ //ibegin或之后某buy日买入
for(int sell(buy+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
earnSingle=prices[sell]-prices[buy]+maxEarn2(prices,sell+1); //当前买入卖出收益+剩下日子里收益的最大值(递归子问题)
res=max(res,earnSingle); //当前所有遍历的最大收益的最大收益
//-1+3-4+5,-1+4,-1+5 -3+4,-3+5 -4+5
//4
}
}
return res;
};
int main(int argc,char *argv[]){
int arr[]={1,2,3,4,5};//{7,1,5,3,6,4};
vector<float> prices;
for(int i(0);i<5;i++) prices.push_back(arr[i]);
std::cout<<maxEarn(prices)<<"\n";
std::cout<<maxEarn2(prices,0)<<"\n";
}
简化:对buy-sell二重遍历的当前步earnSingle,对每个sell,要使earnSingle最大就要使prices[buy]最小,上面是对固定的selll按buy进行遍历,对每个buy计算prices[sell]-prices[buy]+maxEarnViolate(prices,sell+1);后再求最大值,重复计算了maxEarnViolate(prices,sell+1)。其实只需要对固定的sell,找到其之前[ibegin,sell]时间段内最低股价lowbuy进行买入,只计算一次prices[sell]-lowbuy+maxEarnViolate(prices,sell+1); 为此,对每个sell内执行:for (int buy(ibegin);buy<=sell;buy++) lowbuy=min(lowbuy,prices[buy]); 又由于sell是从ibegin+1开始,buy要在ibegin到sell之前买入,所以只需在sell的遍历中维护ibegin到sell之间的一个最低股价lowbuy即可:
float maxEarnViolateSimplify(const vector<float>& prices,int ibegin){ //递归函数,[ibegin:]时间段内收益最大值
if(ibegin>=prices.size()-1) return 0.0f; //最后一天如果买入就不能卖出,不能做交易,挣不了钱
float res(0.0f); //当前步的所有穷举分支earnSingle的最大值的最大值
float earnSingle(0.0f);
// for(int buy(ibegin);buy<prices.size();buy++){ //ibegin或之后某buy日买入,穷举
// for(int sell(buy+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
//简化:在sell卖出日之前买入,maxEarnViolate(prices,sell+1)是恒定的,为了使earnSingle最大,
//只需要在sell卖出日之前的股价最低日买入,也就是ibegin到sell之间的最低股价
float lowbuy(prices[ibegin]);
for(int sell(ibegin+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
// for(int buy(ibegin);buy<=sell;buy++) lowbuy=min(lowbuy,prices[buy]);
lowbuy=std::min(lowbuy,prices[sell]); //在sell遍历过程中始终维持ibegin到sell之间的最低股价,
//如果prices[sell]才最低,此之前ibegin~sell-1日股价都高,那就不应该交易,不赚总比亏了好,
//也即:如果lowbuy==prices[sell],那sell之前"不交易"相当于赚了prices[sell]-lowbuy=0元,不消耗交易计数!
// earnSingle=prices[sell]-prices[buy]+maxEarnViolate(prices,sell+1);
earnSingle=prices[sell]-lowbuy+maxEarnViolate(prices,sell+1);
//当前穷举分支当前一步买入卖出的收益+剩下日子里[sell+1:]收益的最大值(递归子问题)
//当前步问题(ibegin及之后)最优解取决于各个穷举分支的最优解的最优解
res=max(res,earnSingle); //当前问题所有遍历的最大收益的最大收益
//-1+3-3+4-4+5,-1+3-3+4,-1+3-4+5,-1+4,-1+5 -3+4-4+5,-3+4,-3+5 -4+5
//4
}
// }
return res;
};
注意到:如果prices[sell]比前面[ibegin,sell-1]的股价都要低,那此时只要交易就会亏,既使[ibegin,sell-1]之间有个相同价格可以买入,也只是不亏也不赚。所以此时earnSingle=prices[sell]-lowbuy+maxEarnViolate(prices,sell+1);的当前交易prices[sell]-lowbuy不亏不赚,还不如不交易,浪费一次交易次数。这在后面限制交易次数时要格外注意此时选择不交易不浪费一次交易机会!
低阶maxEarn2(prices,i)可能被重复求解,因此可以缓存下maxEarn2(prices,i):
int maxEarn3(const vector<int>& prices,int ibegin){ //递归函数
static vector<int> memo(prices.size(),-1); //记录maxEarn3(prices,i),
if(memo[ibegin]!=-1) return memo[ibegin];
if(ibegin>=prices.size()-1) return 0.0f; //最后一天如果买入就不能卖出,不能做交易,挣不了钱
int res(0.0f);
int earnSingle(0.0f);
for(int buy(ibegin);buy<prices.size();buy++){ //ibegin或之后某buy日买入
for(int sell(buy+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
earnSingle=prices[sell]-prices[buy]+maxEarn3(prices,sell+1); //当前买入卖出收益+剩下日子里收益的最大值(递归子问题)
//当前问题最优值取决于子问题的最优值,maxEarn3(prices,i)可能被重复求值
res=max(res,earnSingle); //当前问题所有遍历的最大收益的最大收益
//-1+3-4+5,-1+4,-1+5 -3+4,-3+5 -4+5
//4
}
}
memo[ibegin]=res;
return res;
};
4、股票问题,限定交易次数k
股票问题升级版:
给定一个数组,它的第i个元素是一支给定股票在第i天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成K笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
例如,输入:[3,2,6,5,0,3],k=2
输出:-2+6-0+3=7
4.1 技巧型解法
思路:限制了交易次数,由于我们是预知未来的上帝视角,所以股价走势已经在眼前,每一个短线递增子数组[a,b]且a<b都是可以交易的,对平线a=b就不浪费交易次数了,不算做短线递增子数组,整个股价价格数组可以统计出m个短线递增区间,能赚的最多利润就是这m次交易的价差和。如果允许交易次数n>=m那直接对m个短线递增区间求价差和。如果n<m就要对m个短线递增区间进行m-n次合并,一次合并为了降低损失,对{[a,b],[c,d]}相邻递增子区间,一定有b>=c,因为一旦b<c那递增区间[b,c]一定会被统计到,b=c时合并两个递增区间没有损失,b>c时就会损失b-c的价差利润,因此每次合并子区间都需要遍历所有相邻子区间,找到损失价差利润最少也就是b-c最小的两个子区间合并,然后同样的方式进行下一次合并,,,一共合并m-n次后得到n个递增子区间就是最优交易。如果某{[a,b],[c,d]}与{[e,f],[g,h]}有b-c==f-g,那二者损失一样的价差,赚的只是价差,不在乎股价绝对值,所以合并前面两个区间或后面两个区间都可以
int maxEarnLimitedTimes(const vector<int> &prices,int n){
vector<vector<int> > spaces; //递增区间数组
for(int i(0);i<prices.size()-1;i++){
if(prices[i]<prices[i+1]){
vector<int> temp;
temp.push_back(prices[i]);
temp.push_back(prices[i+1]);
spaces.push_back(temp);
}
}
int res(0);
int m(spaces.size());
typedef vector<vector<int> > T;
if(n>=m){
for(const vector<int> &spacei:spaces) res+=(spacei[1]-spacei[0]);
return res;
}else{
for(int i(0);i<m-n;i++){ //要合并m-n次最终得到n个子区间
int minJiaCha(1<<31); //最小价差
T::iterator itepre; //最小价差的前一个区间迭代器
T::iterator iteend(spaces.end());
for(T::iterator ite(spaces.begin());ite!=--iteend;ite++){
if((*ite)[1]-(*(ite+1))[0]<minJiaCha) {
minJiaCha=(*ite)[1]-(*(ite+1))[0];
itepre=ite; //最小价差,最小损失
}
}
(*itepre)[1]=(*(itepre+1))[1];
spaces.erase(++itepre); //把合并后的后一个区间删掉
}
//合并到剩n个子区间
for(const vector<int> &spacei:spaces) res+=(spacei[1]-spacei[0]);
return res;
}
};
4.2 暴力穷举+递归,限制交易次数
在暴力穷举的递归调用中传递一个交易次数信息,每交易一次都累加交易次数,当交易次数超过允许次数直接中断之后的交易穷举,返回0.0f,表示之后由于不能交易也就赚不了钱。
int maxEarnViolateTimes2(const vector<int>& prices,int ibegin,int nCount,int times){ //递归函数,[ibegin:]时间段内收益最大值
if(nCount>times)return 0.0f; //已经超过交易次数就不能再交易了
if(ibegin>=prices.size()-1) return 0.0f; //最后一天如果买入就不能卖出,不能做交易,挣不了钱
int res(0.0f); //当前步的所有穷举分支earnSingle的最大值的最大值
int earnSingle(0.0f);
for(int buy(ibegin);buy<prices.size();buy++){ //ibegin或之后某buy日买入,穷举
for(int sell(buy+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
if(prices[sell]<=prices[buy]) earnSingle=0+maxEarnViolateTimes2(prices,sell,nCount+1,times); //交易不赚或者亏,那就不交易,不浪费一次交易机会
else earnSingle=prices[sell]-prices[buy]+maxEarnViolateTimes2(prices,sell+1,nCount+1,times); //交易能赚才交易
//当前穷举分支当前一步买入卖出的收益+剩下日子里[sell+1:]收益的最大值(递归子问题)
//当前步问题(ibegin及之后)最优解取决于各个穷举分支的最优解的最优解
res=max(res,earnSingle); //当前问题所有遍历的最大收益的最大收益
//-1+3-3+4-4+5,-1+3-3+4,-1+3-4+5,-1+4,-1+5 -3+4-4+5,-3+4,-3+5 -4+5
//4
}
}
return res;
};
或者维护一个剩余交易次数值,每交易一次都减1,直到交易次数用完时中断之后的交易穷举,返回0.0f,表示之后由于不能交易也就赚不了钱。
float maxEarnViolateTimes3(const vector<float>& prices,int ibegin,int nCount){ //递归函数,[ibegin:]时间段内收益最大值
if(nCount<=0)return 0.0f; //已经超过交易次数就不能再交易了
if(ibegin>=prices.size()-1) return 0.0f; //最后一天如果买入就不能卖出,不能做交易,挣不了钱
float res(0.0f); //当前步的所有穷举分支earnSingle的最大值的最大值
float earnSingle(0.0f);
for(int buy(ibegin);buy<prices.size();buy++){ //ibegin或之后某buy日买入,穷举
for(int sell(buy+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
if(prices[sell]<=prices[buy]) earnSingle=0+maxEarnViolateTimes3(prices,sell+1,nCount); //交易不赚或者亏,那就不交易,不浪费一次交易机会
else earnSingle=prices[sell]-prices[buy]+maxEarnViolateTimes3(prices,sell+1,nCount+1); //交易能赚才交易
//当前穷举分支当前一步买入卖出的收益+剩下日子里[sell+1:]收益的最大值(递归子问题)
//当前步问题(ibegin及之后)最优解取决于各个穷举分支的最优解的最优解
res=max(res,earnSingle); //当前问题所有遍历的最大收益的最大收益
//-1+3-3+4-4+5,-1+3-3+4,-1+3-4+5,-1+4,-1+5 -3+4-4+5,-3+4,-3+5 -4+5
//4
}
}
return res;
};
5、股票问题,设置冷冻期
给定一个整数数组,其中第i个元素代表了第i天的股票价格。
设计一个算法计算出最大利润。在满足以下约束条件下,你
可以尽可能地完成更多的交易(多次买卖一支股票):
i、你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
也就是在两次购买之间不能保留上一次购买的股票,必须全部卖出。
ii、卖出股票后,你无法在第二天买入股票(即冷冻期为1天)。
示例:
输入:[1,2,3,0,2]
输出:3
解释:对应的交易状态为:[买,卖,冷冻,买,卖]
思路:如果是股票问题一次交易、无限次交易、限定次数交易(合并相邻上涨区间)还能有技巧性解法。
但这里在两次交易之间设置冷冻期至少1天,也就是说在buy日买入sell日卖出后下一次最早也只能
在sell+2日买入了。这显然可以在缩小问题规模的穷举递归解法中通过控制剩余数组规模来实现:
float maxEarnViolateCalmdown(const vector<float>& prices,int ibegin){ //递归函数,[ibegin:]时间段内收益最大值
if(ibegin>=prices.size()-1) return 0.0f; //最后一天如果买入就不能卖出,不能做交易,挣不了钱
float res(0.0f); //当前步的所有穷举分支earnSingle的最大值的最大值
float earnSingle(0.0f);
for(int buy(ibegin);buy<prices.size();buy++){ //ibegin或之后某buy日买入,穷举
for(int sell(buy+1);sell<prices.size();sell++){ //buy+1或之后某sell日卖出,穷举
earnSingle=prices[sell]-prices[buy]+maxEarnViolate(prices,sell+2);
//当前穷举分支当前一步买入卖出的收益+剩下日子里[sell+1:]收益的最大值(递归子问题)
//当前步问题(ibegin及之后)最优解取决于各个穷举分支的最优解的最优解
res=max(res,earnSingle); //当前问题所有遍历的最大收益的最大收益
//-1+3-3+4-4+5,-1+3-3+4,-1+3-4+5,-1+4,-1+5 -3+4-4+5,-3+4,-3+5 -4+5
//4
}
}
return res;
};
6、股票问题,含有每次交易手续费
6.1 技巧型解法
给定一个整数数组prices,其中第i个元素代表了第i天的股票价格;非负整数fee代表了交易股票的手续费用。你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
思路:如果是无限次交易且没有每次交易的手续费,那无非就是累加所有递增子区间,分开买卖至少比合并买卖一样利润(连续递增,或中间持平)或更多(阶段递增,中间降低)。然而当有每次交易的手续费后,对连续递增或中间持平,分开交易与合并交易原本利润相同,但多了手续费,所以必须合并1次交易。对阶段递增{[a,b],[c,d]},分开交易比合并交易多赚了一份b-c但也多交了一份手续费fee,此时如果b-c>fee那还是分开交易赚更多,否则如果b-c<=fee那就合并交易。详见图示
执行:1、先找出所有递增子区间;
2、考察前后子区间[a,b]、[c,d],当且仅当b-c>fee时才分别执行这两个子区间的交易;如果b-c<=fee那就合并前后两个子区间的交易为一个交易[a,d]
int maxEarnWithFee(const vector<int> &prices,int fee){
//首先收集递增子区间,不考虑手续费的话对每个递增子区间进行交易的总利润总是大于等于
//合并交易的总利润
typedef vector<vector<int> > T;
T spaces;
for(int i(0);i<prices.size()-1;i++){
if(prices[i]<prices[i+1]){
vector<int> temp;
temp.push_back(prices[i]);
temp.push_back(prices[i+1]);
spaces.push_back(temp);
}
}
//考察前后子区间的b-c若<=fee就合并前后子区间交易
typedef vector<vector<int> >::iterator iteType;
iteType iteLast(spaces.end());
--iteLast;
for(iteType itePre(spaces.begin());itePre!=iteLast;){
if((*itePre)[1]-(*(itePre+1))[0]<=fee){ //合并两个交易子区间成1次交易
(*itePre)[1]=(*(itePre+1))[1];
spaces.erase(itePre+1);
iteLast=spaces.end(); //更新尾后迭代器
--iteLast;
//当前子区间与后面子区间合并了,itePre保持不变,继续考察下个子区间是否与当前子区间合并
continue;
}
itePre++; //当前交易子区间不合并,考虑下个
continue;
//b-c>fee,分开交易赚更多,保留分开的交易子区间
}
//累加子区间交易价差
int total(0);
for(iteType ite(spaces.begin());ite!=spaces.end();ite++){
total+=(*ite)[1]-(*ite)[0]-fee;
}
return total;
}
6.2 暴力穷举+递归,直接使穷举的每次交易增加手续费,比较穷举的利润最大值
int maxEarnWithFeeViolate(const vector<int> &prices,int ibegin,const int& fee){
//暴力穷举+递归
//再复习一遍:递归将问题规模缩小,并且小规模问题具有确定解,作为边界值,大规模问题可由小规模问题递推
//暴力穷举,穷举第一组buy、sell能算出第一笔交易结果,后面就在缩小了的prices[sell+1:]价格数组内继续
//穷举其第二组buy、sell交易结果。。。当价格数组规模缩小到prices[len-2,len-1]时就只有len-2买入
//len-1卖出一种情况,交易结果为prices[len-1]-prices[len-2],当缩小到prices[len-1]时不能买入
//,也就是ibegin>=len-1时后面就无法继续交易了,也可以说后面所能获取的交易结果是0
if(ibegin>=prices.size()-1) return 0;
int maxEarn(-1*(1<<30));
int earnSingle(0);
for(int buy(ibegin);buy<prices.size()-1;buy++){ //
for(int sell(buy+1);sell<prices.size();sell++){
earnSingle=prices[sell]-prices[buy]-fee+maxEarnWithFeeViolate(prices,sell+1,fee);
maxEarn=max(maxEarn,earnSingle); //穷举可能存在利润负的情况,比如一路下跌,所以maxEarn初始化之极小
}
}
return maxEarn;
}
7、组合数问题
/*
动态规划的递推公式形态多变
给定整数数组比如[1 1 1 1 1]使在每个元素之前使用+、-符号,使最终的数组和等于3,
问有多少种组合?
思路:数组,动态规划,动态规划数组下标的物理意义
首先把问题抽象为"使下标0~i的数组元素和为j的组合数",有两个变量i、j,也就是dp[i][j],
动态规划思想就是考察当前所求的dp[i][j]与其附近dp[i-1][j]等的递推关系,以实现从已知
边界通过递推关系递推计算出待求坐标处解的目的。
考虑dp[i][j],0~i下标元素的和等于j的组合数,应该与0~i-1下标元素的和等于j-arr[i]或j+arr[i]
有关。其实,0~i下标元素的和等于j的组合数就是0~i-1下标元素的和等于j-arr[i]的组合数
再加0~i-1下标元素的和等于j+arr[i]的组合数。
这里子数组总是要从下标0开始,不像其他动规子数组问题比如"最长递增子序列"那样允许子数组从
输入数组中间切片,从而dp[i][j]
*/