C/C++ 动态规划随笔

礼物的最大价值

题目参考LeetCode

问题描述:

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

问题分析:

很容易想到这是一个动态规划基础题目,核心思想就是找到状态转移方程,简单分析以下可以知道这里面的自变量只有两个,分别是行坐标和列坐标,而因变量就是当前的价值。

非边界条件下的状态转移方程为:(row < m && col < n)
F[row][col] = max{F[row-1][col],F[row][col-1]} + V[row][col]
边界条件下的状态转移方程为:
1.row == m
F[row][col] = F[row-1][col] + V[row][col]
2.col == n
F[row][col] = F[row][col-1] + V[row][col]

代码展示:

#include<bits/stdc++.h> 
using namespace std;

int main(){
     
	int m,n;
	puts("请输入矩阵的行数和列数:");
	scanf("%d%d",&m,&n);
	int V[m][n];
	for(int i = 0 ; i < m ; i++){
     
		for(int j  = 0 ; j < n ;j++){
     
			scanf("%d",&V[i][j]);
		}
	}
	int F[m][n];
	memset(F,0,sizeof(F));
	F[0][0] = V[0][0];
	// 边界条件 
	for(int i = 1 ; i<m;i++){
     
		F[i][0] = V[i][0]+F[i-1][0];
	}
	for(int i = 1;i<n;i++){
     
		F[0][i] = V[0][i]+F[0][i-1];	
	}	
	for(int i = 1;i <  m;i++){
     
		for(int j = 1; j < n;j++){
     
			F[i][j] = max(F[i-1][j],F[i][j-1])+V[i][j]; 
		}
	}
	
	int path[m][n];
	memset(path,0,sizeof(path));
	int r = m-1;
	int c = n-1;
	int temp;
	path[r][c] = 1;
	do{
     
		temp = F[r][c] - V[r][c];
		if(r-1>=0 && temp == F[r-1][c]){
      // 上方 
			path[r-1][c] = 1;
			r--; // 更新行数 
		}else if(c-1>=0 && temp == F[r][c-1]){
      // 下方 
			path[r][c-1] = 1;
			c--; // 更新列数 
		}
		if(r == 0 && c == 0){
      // 退出循环条件 
			break;
		}
	}while(true);
	
	cout<<"最大价值为:"<<F[m-1][n-1]<<endl;
	printf("输出路径为: \n");
	for(int i = 0 ; i<m ;i++){
     
		for(int j = 0 ; j<n ; j++){
     
			printf("%d ",path[i][j]);
		}
		puts("");
	}
	return 0;
}

零钱兑换

题目参考LeetCode

问题描述:

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

问题分析:

典型的动态规划题目,采用自底向上的方法求解,首先分析易知子问题为兑换所有种类的硬币后还有多少余额,如果无法兑换则将其设为无穷大。

状态转移方程为:
f[x] = min{f[x-c1],f[x-c2],f[x-c3],...,f[x-cn]} + 1
其中如果x

代码展示:

#include<bits/stdc++.h>
using namespace std;
#define INF 1e6

int main() {
     
	int n;
	printf("请输入硬币的种类数:\n");
	scanf("%d",&n);
	int coins[n];
	for(int i = 0 ; i < n ; i++) {
     
		scanf("%d",&coins[i]);
	}
	printf("请输入需要换取的总金额:\n");
	int s;
	scanf("%d",&s);
	int temp[n];
	memset(temp,0,sizeof(temp));
	int f[s+1];
	int proc[s+1]; // 记录每次兑换的最小值
	memset(f,0,sizeof(f));
	for(int i = 1 ; i <= s ; i++) {
     
		for(int j = 0 ; j < n ; j++) {
     
			if(coins[j] > i) {
     
				temp[j]	 = INF; // 无穷大
			} else {
     
				temp[j] = f[i - coins[j]]; // 当前条件说明可以换取硬币
			}
		}
		int* minPos = min_element(temp,temp+n);
		f[i] = *minPos + 1; // 状态转移公式

		proc[i] = int(minPos - temp); // 获取最小值的下标
 	}
	// 判断是否兑换方案可行
	if(f[s] < INF) {
     
		printf("最少兑换的硬币个数为:%d\n",f[s]);
		printf("可选方案为:\n");
		int ind = s;
		int result[s+1],k = 0; // 保存方案结果 
		
		while(f[ind]) {
     
			result[k++] = coins[proc[ind]];
			ind -= coins[proc[ind]];
		}
		// 输出方案结果 
		for(int i = 0 ; i<k ; i++){
     
			printf("%d ",result[i]);
		}
	} else {
     
		printf("无方案");
	}
	return 0;
}

打家劫舍

题目参考LeetCode

问题描述:

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动
警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

问题分析:

状态转移方程为:
steal[k] = max(steal[k-1],steal[k-2]+house[k])

代码展示:

#include<bits/stdc++.h>
using namespace std;
int main(){
     
	printf("请输入房子个数:\n");
	int n;
	scanf("%d",&n);
	int house[n];
	for(int i = 0 ; i<n ; i++){
     
		scanf("%d",&house[i]);
	}
	int steal[n];
	memset(steal,0,sizeof(steal));
	steal[0] = 1; // 只有前一家的情况
	steal[1] = max(house[0],house[1]); // 只有前两家选择最多的
	for(int i = 2 ; i<sizeof(house)/sizeof(int);i++){
     
		steal[i] = max(steal[i-1],steal[i-2]+house[i]); // 状态转移方程 
	}
	printf("偷窃的最大钱财为:%d",steal[n-1]); 
	return 0;
}

迷路的机器人

题目参考LeetCode

问题描述:

设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种方法,寻找机器人从左上角移动到右下角的路径。
网格中的障碍物和空位置分别用 1 和 0 来表示。

返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 0 行 0 列。如果没有可行的路径,返回空数组。

问题分析:

这里某一个格子的方案个数取决于上一个格子的方案数和左边格子的方案数,故状态转移公式:
当此时的格子不为障碍物体时:
	square[row][col] = square[row-1][col] + square[row][col-1]
当此时的格子为障碍物体时:
	square[row][col] = 0
	
边界条件:
row == 1:
	遇到障碍物体前全为1,遇到障碍物体后全为0
col == 1:
	遇到障碍物体前全为1,遇到障碍物体后全为0

代码展示:

#include<bits/stdc++.h>
using namespace std;
int main(){
     
	int m,n;
	printf("请输入行数和列数:\n"); 
	scanf("%d%d",&m,&n);
	int square[m][n],temp[m][n];
	// 1为放置障碍物 0为未防止障碍物 
	printf("请输入障碍物放置位置:\n"); 
	for(int i = 0 ; i<m ; i++){
     
		for(int j = 0 ; j<n ; j++){
     
			scanf("%d",&temp[i][j]);
		}
	}
	memset(square,0,sizeof(square));
	square[0][0] = 1; // 起点只有一种情况 
	// 边界条件 这里有坑 
	bool flag = false; 
	for(int i = 1 ; i < n ; i++){
     
		if(temp[0][i]) {
     
			flag = true;	
		} 
		if(flag){
     
			continue; 
		}else{
     
			square[0][i] = 1;
		}
	}
	for(int i = 1 ; i < m ; i++){
     
		if(temp[i][0]){
     
			flag = false;
		}
		if(!flag){
     
			continue;
		}else{
     
			square[i][0] = 1;
		}
	}
	// 非边界条件
	for(int i = 1 ; i<m ; i++){
     
		for(int j = 1 ; j<n ; j++){
     
			// 当前格子的方案数等于上一个格子的方案数加上左边格子的方案数
			if(temp[i][j]){
      // 当前位置为障碍物 
				square[i][j] = 0; 
			}else{
      // 当前位置不为障碍物 
				square[i][j] = square[i-1][j]+square[i][j-1];
			}
		}
	}
	// 输出结果 
	printf("机器人走到右下角的方案数为:%d",square[m-1][n-1]);
	return 0;
}

掷骰子的N种方法

题目参考LeetCode

问题描述:

这里有 d 个一样的骰子,每个骰子上都有 f 个面,分别标号为1, 2, …, f。
我们约定:掷骰子的得到总点数为各骰子面朝上的数字的总和。

如果需要掷出的总点数为 target,请你计算出有多少种不同的组合情况(所有的组合情况总共有 f^d 种),模 10^9 + 7 后返回。

问题分析:

容易分析了解到当抛掷前i个骰子且其点数和为j时为原问题的子问题,
特别地,当i=m,j=s时为原问题,其状态转移方程为:
F[i][j] = sum{F[i-1][j-1],F[i-1][j-2],...,F[i-1][j-f]}
即抛掷前i个骰子且其点数和为j时的方案个数等于抛掷前i-1个骰子,
且其点数和为j-k(k=1,2,...,f)的方案个数
对于更多细节,读者可以通过以下的代码在草稿纸上模拟

代码展示:

#include<bits/stdc++.h>
using namespace std;
int main(){
     
	int m,f,s;
	printf("请输入骰子个数,每个骰子的面数,总点数:"); 
	scanf("%d%d%d",&m,&f,&s);
	if(m == 1){
      // 单独考虑一个骰子的情况
		printf("方案个数为:%d",f >= s ? 1 : 0);
		return 0;
	}
	
	int F[m][s]; // 骰子为m个,总点数为s时的方案个数 
	memset(F,0,sizeof(F));
	// 边界条件
	// 当骰子个数为 1 
	for(int i = 0 ; i<s ;i++){
     
		if(i<f){
     
			F[0][i] = 1;
		}
	} 
	// 当总点数为 1 F[i-1][1-1] = 0 for i in m
	// 非边界条件
	for(int i = 1 ; i<m ; i++){
     
		for(int j = 1 ; j<s ; j++){
     
			for(int k = 1 ; k <= f ; k++){
     
				if(j < k){
      
					break;
				}else{
     
					F[i][j] = (F[i][j]+F[i-1][j-k])%(1000000007); // 加上上一次方案数 并取模 
				}
			}
		}
	}
	printf("方案数为:%d\n",F[m-1][s-1]);
	return 0;
}

编辑距离

题目参考LeetCode

题目描述:

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

题目分析:

首先要理解该题自变量只有两个,即F[i][j]为word1前i个单词转换为word2的前j个单词的操作数,
考虑边界条件:
	i == 0:
	即word1的第一个单词转换为word2的前j个单词的操作数
	这里有两种情况:
		一种是word2的前j个单词包括了word1的第一个单词 F[0][j] = j-1
		另一种是没有包括 F[0][j] = j;
	由对称性可知:
	j == 0:
	同样有两种情况:
		一种是word2的前j个单词包括了word1的第一个单词 F[i][0] = i-1
		另一种是没有包括 F[i][0] = i;
其次非边界条件:
	由于只有三种操作,故分三种情况讨论:
		替换操作:temp1 = F[i-1][j-1] + (word1[i]==word2[j]?0:1);
		插入操作:temp2 = F[i][j-1] + 1;
		删除操作:temp3 = F[i-1][j] + 1;
		
		最终求:F[i][j] = min(temp1,temp2,temp3);

代码展示:

#include<bits/stdc++.h>
using namespace std;
int main(){
     
	char word1[501];
	char word2[501];
	printf("请输入两个字符串:\n");
	scanf("%s %s",&word1,&word2);
	
	int n = strlen(word1), m = strlen(word2);
	int F[n][m]; // 最少的变换次数 
	memset(F,0,sizeof(F));
	// 边界条件 
	// 由word1的第一个字母变换为word2的前i+1个字母
	for(int i = 0 ; i < m ; i++){
     
		int index = -1; // 判断word2前i+1个字母是否包含word1的首字母
		for(int j = 0; j <= i ; j++){
     
			if(word1[0] == word2[j]){
     
				index = j; 
				break;
			}
		}
		F[0][i] = index>=0 ? i:i+1;
	}
	// 由word1的前i+1个字母变为word2的第一个字母 
	for(int i = 0 ; i < n ; i++){
     
		int index = -1;
		for(int j = 0 ; j <= i; j++){
     
			if(word2[0] == word1[j]){
     
				index = j;
				break;
			} 
		}
		F[i][0] = index>=0 ? i:i+1;
	}
	
	//非边界条件
	for(int i = 1 ; i<n ; i++){
     
		for(int j = 1 ; j<m ; j++){
     
			int temp1 = F[i-1][j-1]
			 + (word1[i] == word2[j] ? 0:1); // 判断是否相等,如果相等则不做人任何操作否则替换 
			int temp2 = F[i-1][j] + 1; // 一次删除操作 删除word1中一个单词 
			int temp3 = F[i][j-1] + 1; // 一次插入操作 插入word2中一个单词 
			
			F[i][j] = min(min(temp1,temp2),temp3); //状态转移方程 最终求所有可行操作的最小值 
		}
	}
	printf("变换的最少操作数为:%d",F[n-1][m-1]); 
	return 0;
}

你可能感兴趣的:(动态规划)