1.机器人移动
问题:
int N:N个格子(1~N)
int s:开始位置
int E:结束位置
int k:机器人必须走k步
机器人移动方式:向左或向右一格,但不能越界
求:总共有几种方式,用k步从s到e
方法一:普通版
/*
N:一共是N个位置
E:终点
rest:还剩rest步要走,rest初始值其实就是k
cur:当前位置
反回方法数
*/
int f1(int N, int E, int rest, int cur) {
if (rest == 0) {
return cur == E ? 1 : 0;
}
//防止越界,强行干预机器人移动
if (cur == 1) return f1(N, E, rest - 1, 2);
if (cur == N)return f1(N, E, rest - 1, N - 1);
//中间位置,机器人即可向左走也可向右走
return f1(N, E, rest - 1, cur - 1) + f1(N, E, rest - 1, cur + 1);
}
int walkWays1(int N, int E, int S, int K) {
return f1(N, E,K,S);
}
方法一的问题:由于f1函数只和rest、cur这两个变量有关,而每个格子(除边界格子)又可能从左边或右边来,因此子
过程可能会有重复的f1函数。
时间复杂度分析:
机器人既可往左也可往右,类似二叉树,总共有k步,所以是一棵高度为k的二叉树,那么时间复杂度就是 O(2^k)
方法二:优化版(利用可变参数做二维表)
int f2(int N, int E, int rest, int cur,vector<vector<int>>&dp) {
if (dp[rest][cur] != -1) {//如果算过,直接返回算过的返回值
return dp[rest][cur];
}
//缓存未命中
if (rest == 0) {
dp[rest][cur] = cur == E ? 1 : 0;
return dp[rest][cur];
}
//防止越界,强行干预机器人移动
if (cur == 1) {
dp[rest][cur] = f2(N, E, rest - 1, 2, dp);
}
else if (cur == N) {
dp[rest][cur] = f2(N, E, rest - 1, N-1, dp);
}
else {//中间位置,机器人即可向左走也可向右走
dp[rest][cur]= f2(N, E, rest - 1, cur - 1, dp) + f2(N, E, rest - 1, cur + 1, dp);
}
return dp[rest][cur];
}
int walkWays2(int N, int E, int S, int K) {
vector < vector<int>>dp(K + 1, vector<int>(N + 1,-1));
return f2(N, E,K,S,dp);
}
时间复杂度分析:
dp的大小是k*N,而求每个dp的时间复杂度是 O(1),因此,总时间复杂度就是 O(k*N)
2.最少硬币数
给定不同面额的硬币 coins (硬币数各只有一个)和一个总金额 amount。编写一个函数来计算可以凑成
总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
方法一:普通版
//返回组成rest的最少硬币数
int process1(vector<int>& coins, int index, int rest) {
if (rest < 0) {//该枚硬币不可取,删去
return -1;
}
if (rest == 0) {//该枚硬币取完之后刚好组成aim,但因为已经算过该枚硬币了,所以这里返回0
return 0;
}
if (index == coins.size()) {//越界,因此多算了一枚,要删去
return -1;
}
int p1 = process1(coins, index + 1, rest);//不取
int p2Next = process1(coins, index + 1, rest - coins[index]);//取
if (p1 == -1 && p2Next == -1) {
return -1;//无效解
}
else {
if (p1 == -1)return 1 + p2Next;
if (p2Next == -1)return p1;
}
return min(p1, 1 + p2Next);
}
int minCoins1(vector<int>& coins, int aim) {
return process1(coins, 0, aim);
}
方法二:优化版
int process2(vector<int>& coins, int index, int rest,vector<vector<int>>&dp) {
if (rest < 0) {//该枚硬币不可取,删去
return -1;
}
if (dp[index][rest] != -2) {
return dp[index][rest];
}
if (rest == 0) {//该枚硬币取完之后刚好组成aim,但因为已经算过该枚硬币了,所以这里返回0
dp[index][rest] = 0;
}
if (index == coins.size()) {//越界,因此多算了一枚,要删去
dp[index][rest] = -1;
}
else {
int p1 = process2(coins, index + 1, rest, dp);//不取
int p2Next = process2(coins, index + 1, rest - coins[index], dp);//取
if (p1 == -1 && p2Next == -1) {
dp[index][rest] = -1;//无效解
}
else {
if (p1 == -1) {
dp[index][rest] = 1 + p2Next;
}
else if (p2Next == -1) {
dp[index][rest] = p1;
}
else {
dp[index][rest] = min(p1, 1 + p2Next);
}
}
}
return dp[index][rest];
}
int minCoins2(vector<int>& coins, int aim) {
vector<vector<int>>dp(coins.size() + 1, vector<int>(aim + 1, -2));
return process2(coins, 0, aim, dp);
}
方法三:动态规划
int minCoins3(vector<int>coins, int aim) {
int N = coins.size();
vector<vector<int>>dp(N + 1, vector<int>(aim + 1));
for (int col = 1; col <= aim; col++) {
dp[N][col] = -1;
}
for (int index = N - 1; index >= 0; index--) {
for (int rest = 1; rest <= aim; rest++) {
int p1 = dp[index + 1][rest];
int p2Next = -1;
if (rest - coins[index] >= 0) {//防止越界
p2Next = dp[index + 1][rest - coins[index]];
}
if (p1 == -1 && p2Next == -1) {
dp[index][rest] = -1;
}
else {
if (p1 == -1) {
dp[index][rest] = p2Next + 1;
}
else if (p2Next == -1) {
dp[index][rest] = p1;
}
else {
dp[index][rest] = min(p1, p2Next + 1);
}
}
}
}
return dp[0][aim];
}
3.马到终点的方法数
问题:
给定一个中国象棋棋盘,马走日,从(0,0)到(a,b)必须走k步的方法数有多少?
思路:
可以反着来,从(a,b)到(0,0),step递减,最终看step=0时,是否到达(0,0)
//防止越界访问数组
int getValue(vector<vector<vector<int>>>& dp, int step, int row, int col) {
if (row < 0 || row>9 || col < 0 || col>8)return 0;
return dp[step][row][col];
}
int dpWays(int i, int j, int step) {
if (j < 0 || j>8 || i < 0 || i>9 || step < 0) {
return 0;
}
vector<vector<vector<int>>>dp(step + 1, vector<vector<int>>(10, vector<int>(9)));
dp[0][0][0] = 1;
for (int h = 1; h <= step; h++) {//层
for (int r = 0; r < 10; r++) {
for (int c = 0; c < 9; c++) {
dp[h][r][c] = getValue(dp, h - 1, r - 1, c + 2);
dp[h][r][c] = getValue(dp, h - 1, r - 1, c - 2);
dp[h][r][c] = getValue(dp, h - 1, r + 1, c + 2);
dp[h][r][c] = getValue(dp, h - 1, r + 1, c - 2);
dp[h][r][c] = getValue(dp, h - 1, r - 2, c + 1);
dp[h][r][c] = getValue(dp, h - 1, r - 2, c - 1);
dp[h][r][c] = getValue(dp, h - 1, r + 2, c + 1);
dp[h][r][c] = getValue(dp, h - 1, r + 2, c - 1);
}
}
}
return dp[step][i][j];
}
4.最少方法数
给定不同面额的硬币 coins (硬币数各有任意个)和一个总金额 amount。编写一个函数来计算可以凑成
总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
方法一:递归版
int process(vector<int>& coins, int index, int rest) {
if (index == coins.size()) {
return rest == 0 ? 1 : 0;
}
int ways = 0;
//张数过大就没意义了,因为已经超出rest了
for (int zhang = 0; coins[index] * zhang < rest; zhang++) {
ways += process(coins, index + 1, rest - coins[index] * zhang);
}
return ways;
}
方法二:动态规划
int way2(vector<int>coins, int aim) {
if (coins.size() == 0)return 0;
int N = coins.size();
vector<vector<int>>dp(N + 1, vector<int>(aim + 1));
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int ways = 0;
for (int zhang = 0; coins[index] * zhang <= rest; zhang++) {
ways += dp[index + 1][rest - coins[index] * zhang];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}
时间复杂度: O(N*aim^2)
优化版:用临近值代替枚举
int way3(vector<int>coins, int aim) {
if (coins.size() == 0)return 0;
int N = coins.size();
vector<vector<int>>dp(N + 1, vector<int>(aim + 1));
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index+1][rest];
if(rest-coins[index]){//防止越界
dp[index][rest]+=dp[index][rest-coins[index]]
}
}
}
return dp[0][aim];
}
时间复杂度: O(N*aim)
总结:
递归(定)=> 记忆搜索 => 严格位置依赖
-
可变参数范围
-
目标值
-
base case
-
中间范围的依赖关系
-
求值顺序