算法课8-Dynamic Programming⭐️

本文详细介绍了动态规划的概念,对比了动态规划与分治法、贪心法的区别,并通过实例探讨了动态规划在解决Weighted interval scheduling、Segmented least squares、买卖股票系列问题中的应用。此外,还讨论了最长递增子序列、合唱队形问题和子序列和最大系列等经典问题的解法。

动态规划是常考算法,将指数级的问题降到多项式级别。

动态规划和分治法都是将问题划分成子问题进行求解,它们的区别主要是:

  • 分治法的子问题无重叠
  • 动态规划的子问题有重叠,并且重叠的个数是指数级别的

动态规划和贪心法的相同之处是原问题包含子问题的最优解,而它们的区别在于:

  • 贪心法只看局部最优,最终达到全局最优
  • 对于动态规划局部最优不一定是全局最优

1. Weighted interval scheduling
带权重的最大兼容子集,不能用贪心法进行求解,需要考虑动态规划的解法。首先将活动按照结束时间排序。两种情况,一种是选当前的j,那么最优解等于从后往前找第一个与j不冲突的活动(使用二分查找加快速度)的opt加上j的权重,一种是不选j,那么最优解等于j的前一个活动的最优解。如下式:
o p t ( j ) = m a x { v j + o p t ( p ( j ) ) , o p t ( j − 1 ) } opt(j)=max\{v_j+opt(p(j)),opt(j-1)\} opt(j)=max{ vj+opt(p(j)),opt(j1)}
ps.若按照开始时间排序,要反过来找,那么opt(i)可以定义为,i~n个活动,p(i)是从前往后找第一个不冲突的活动。opt(n+1)=0,return opt(1)。
o p t ( i ) = m a x { v i + o p t ( p ( i ) ) , o p t ( i + 1 ) } opt(i)=max\{v_i+opt(p(i)),opt(i+1)\} opt(i)=max{ vi+opt(p(i)),opt(i+1)}

2. Segmented least squares
线段拟合点集问题,希望线段尽量少,error尽量小。
error定义为E+cL ,E是平方误差之和,L是线段条数。
opt(j) 是 p 1 . . . p j p_1...p_j p1...pj的最小error,e(i,j)是 p i , p i + 1 . . . p j p_i,p_{i+1}...p_j pi,pi+1...pj的最小平方误差
o p t ( j ) = min ⁡ 1 ≤ i ≤ j { e i j + c + o p t ( i − 1 ) } opt(j)=\min \limits_{1\leq i\leq j}\{e_{ij}+c+opt(i-1)\} opt(j)=1ijmin{ eij+c+opt(i1)}
这个算法的瓶颈在于计算 e i j e_{ij} eij,对j要扫一遍,j固定了,i要扫一遍,i固定了,在i j之间要扫一遍, 使得算法的效率是 O ( n 3 ) O(n^3) O(n3).

3. 买卖股票系列

  • 只能买卖一次
    Best Time to Buy and Sell a Stock
    f(j):第j次卖出的最优解, f(1)=0, return max ⁡ 1 ≤ j ≤ n ( f ( j ) ) \max\limits_{1\leq j\leq n}(f(j)) 1jnmax(f(j)) ,两种情况,第一种是当天卖出,第二种是前j-1天买入,第j天卖出,第二种情况和第j-1天卖出的区别就是第j天的价格减去第j-1天的价格。
    f ( j ) = m a x ( 0 , f ( j − 1 ) + p j − p j − 1 ) f(j)=max(0,f(j-1)+p_{j}-p_{j-1}) f(j)=max(0,f(j1)+pjpj1)
    一个直观的想法是说维护到目前为止价格的最低点,用上一行减去下一行即可得到最大利润。
5 2 7 13 1 9
5 2 2 2 1 1

这道题还可以用分治法做,详见上一篇博文。

  • 买卖两次
    Best Time to Buy and Sell Stock III
    设置两个dp数组, f 1 ( i ) f_1(i) f1(i)是第i天第一次卖出最大收益, f 2 ( i ) f_2(i) f2(i)是第i天第二次卖出最大收益。
    思路1:
    g 1 ( j ) = max ⁡ 1 ≤ i ≤ j f 1 ( i ) g_1(j)=\max\limits_{1\leq i \leq j}{f_1(i)} g1(j)=1ijmaxf1(i) 即为前j天的最大收益
    f 2 ( j ) = max ⁡ 1 ≤ i ≤ j { p j − p i + g 1 ( i − 1 ) } f_2(j)=\max\limits_{1\leq i \leq j}\{p_j-p_i+g_1(i-1)\} f2(j)=1ijmax{ pjpi+g1(i1)}
    时间 θ ( n 2 ) \theta (n^2) θ(n2) 空间 θ ( n ) \theta (n) θ(n)
    思路2:
    f 2 ( j ) = m a x ( g 1 ( j − 1 ) , f 2 ( j − 1 ) + p j − p i ) f_2(j)=max(g1(j-1),f_2(j-1)+p_j-p_i) f2(j)=max(g1(j1),f2(j
import os import cv2 import numpy as np import matplotlib.pyplot as plt import argparse import trimesh from pathlib import Path def normalize_disparity_map(disparity_map): """归一化视差图用于可视化""" disp = np.maximum(disparity_map, 0.0) if disp.max() == 0: return np.zeros_like(disp, dtype=np.uint8) return (disp / disp.max() * 255).astype(np.uint8) def visualize_disparity_map(disparity_map, gt_map=None, save_path=None): """可视化或保存视差图(支持无 GT 模式)""" disp_vis = normalize_disparity_map(disparity_map) if gt_map is not None: gt_vis = normalize_disparity_map(gt_map) concat_map = np.concatenate([disp_vis, gt_vis], axis=1) labels = ['Disparity', 'Ground Truth'] else: concat_map = disp_vis labels = ['Disparity'] if save_path is None: plt.figure(figsize=(10, 4)) plt.imshow(concat_map, cmap='gray') plt.title(' | '.join(labels)) plt.axis('off') plt.show() else: os.makedirs(os.path.dirname(save_path), exist_ok=True) plt.imsave(save_path, concat_map, cmap='gray', vmin=0, vmax=255) print(f"📊 视差图已保存至: {save_path}") def task1_compute_disparity_map_simple( ref_img: np.ndarray, sec_img: np.ndarray, window_size: int, disparity_range: tuple, matching_function: str ): """简单滑动窗口立体匹配""" H, W = ref_img.shape min_disp, max_disp = disparity_range pad = window_size // 2 disparity_map = np.zeros((H, W), dtype=np.float32) # 边界填充 ref_pad = np.pad(ref_img, ((pad, pad), (pad, pad)), mode='constant') sec_pad = np.pad(sec_img, ((pad, pad), (pad, pad)), mode='constant') for h in range(pad, H + pad): for w in range(pad, W + pad): ref_win = ref_pad[h - pad:h + pad + 1, w - pad:w + pad + 1] costs = [] for d in range(min_disp, max_disp): sec_col = w - d if sec_col < pad or sec_col >= W + pad: cost = np.inf else: sec_win = sec_pad[h - pad:h + pad + 1, sec_col - pad:sec_col + pad + 1] if matching_function == 'SSD': cost = np.sum((ref_win - sec_win) ** 2) elif matching_function == 'SAD': cost = np.sum(np.abs(ref_win - sec_win)) elif matching_function == 'normalized_correlation': rw_mean = ref_win - ref_win.mean() sw_mean = sec_win - sec_win.mean() numerator = np.sum(rw_mean * sw_mean) denominator = np.sqrt(np.sum(rw_mean**2) * np.sum(sw_mean**2)) + 1e-8 cost = -numerator / denominator # 越大越好 → 取负号变最小化 else: raise ValueError(f"Unknown function: {matching_function}") costs.append(cost) best_d = np.argmin(costs) + min_disp disparity_map[h - pad, w - pad] = float(best_d) return disparity_map def task1_simple_disparity(ref_img, sec_img, gt_map, img_name='tsukuba'): """执行 Task1:多种参数组合测试(适配Tsukuba视差范围)""" window_sizes = [5, 9] disparity_range = (0, 16) # 修正:Tsukuba实际视差范围是0-16 matching_functions = ['SSD', 'SAD'] results = [] for ws in window_sizes: for mf in matching_functions: print(f"🔧 计算中: ws={ws}, func={mf}") disp_map = task1_compute_disparity_map_simple(ref_img, sec_img, ws, disparity_range, mf) results.append((disp_map, ws, mf, disparity_range)) # 保存结果 dmin, dmax = disparity_range visualize_disparity_map( disp_map, gt_map, save_path=f"output/task1_{img_name}_{ws}_{dmin}_{dmax}_{mf}.png" ) return results def task2_compute_depth_map(disparity_map, baseline=0.1, focal_length=350.0): """由视差图计算深度图 z = fB / d(适配Tsukuba实际参数)""" with np.errstate(divide='ignore', invalid='ignore'): depth_map = (focal_length * baseline) / (disparity_map + 1e-6) depth_map[disparity_map <= 0] = 0 depth_map[depth_map > 50] = 0 # 修正:Tsukuba场景深度不超过50m return depth_map def task2_visualize_pointcloud( ref_img: np.ndarray, disparity_map: np.ndarray, save_path: str = 'output/task2_tsukuba.ply', baseline: float = 0.1, # 修正:Tsukuba基线0.1m(10cm) focal_length: float = 350.0 # 修正:Tsukuba像素焦距350 ): """从视差图生成带颜色的 3D 点云并导出 .ply 文件""" depth_map = task2_compute_depth_map(disparity_map, baseline, focal_length) H, W = ref_img.shape[:2] y_coords, x_coords = np.mgrid[0:H, 0:W] # 构建 3D 坐标(适配Tsukuba相机内参) points = np.stack([ (x_coords - W / 2) * depth_map / focal_length, (y_coords - H / 2) * depth_map / focal_length, depth_map ], axis=-1).reshape(-1, 3) colors = cv2.cvtColor(ref_img, cv2.COLOR_BGR2RGB).reshape(-1, 3) # 过滤有效点(放宽条件,保留更多点) valid_mask = ( ~np.isnan(points[:, 2]) & ~np.isinf(points[:, 2]) & (points[:, 2] > 0.5) & # 去除过近点 (points[:, 2] < 50) # 去除过远点 ) points = points[valid_mask] colors = colors[valid_mask] # 创建点云并保存 os.makedirs(os.path.dirname(save_path), exist_ok=True) pointcloud = trimesh.PointCloud(vertices=points, colors=colors) pointcloud.export(save_path, file_type='ply') print(f"✅ 点云已保存至: {save_path}(共 {len(points)} 个点)") def task3_compute_disparity_map_dp(ref_img, sec_img): """动态规划行级立体匹配(修复窗口索引+适配Tsukuba)""" H, W = ref_img.shape window_size = 5 pad = window_size // 2 max_disp = 16 # 修正:匹配Tsukuba视差范围 lambda_dp = 8 # 边界填充(与Task1一致) ref_pad = np.pad(ref_img, ((pad, pad), (pad, pad)), mode='constant') sec_pad = np.pad(sec_img, ((pad, pad), (pad, pad)), mode='constant') disparity_map = np.zeros((H, W), dtype=np.float32) for h in range(pad, H + pad): # 修正:从pad开始遍历,匹配填充后的窗口 costs = np.full((W, max_disp), np.inf, dtype=np.float32) # 预计算 SSD 成本(修复窗口索引) for w in range(pad, W + pad): ref_win = ref_pad[h - pad:h + pad + 1, w - pad:w + pad + 1] for d in range(max_disp): sec_w = w - d if sec_w < pad or sec_w >= W + pad: continue sec_win = sec_pad[h - pad:h + pad + 1, sec_w - pad:sec_w + pad + 1] costs[w - pad, d] = np.sum((ref_win - sec_win) ** 2) # 动态规划(Viterbi 算法) V = np.zeros((W, max_disp)) # 最小累计成本 parent = np.zeros((W, max_disp), dtype=int) V[0, :] = costs[0, :] for w in range(1, W): for d in range(max_disp): prev_cost = V[w - 1, :] + lambda_dp * np.abs(np.arange(max_disp) - d) best_prev = np.argmin(prev_cost) V[w, d] = costs[w, d] + prev_cost[best_prev] parent[w, d] = best_prev # 回溯最优路径 cur_d = np.argmin(V[-1, :]) path = [] for w in range(W - 1, -1, -1): path.append(cur_d) cur_d = parent[w, cur_d] path.reverse() disparity_map[h - pad, :] = path return disparity_map def main(tasks): script_dir = Path(__file__).resolve().parent data_dir = script_dir / "data" output_dir = script_dir / "output" output_dir.mkdir(exist_ok=True) def load_image(filename, grayscale=False): filepath = data_dir / filename if not filepath.exists(): print(f"❌ 文件不存在: {filepath}") return None flag = cv2.IMREAD_GRAYSCALE if grayscale else cv2.IMREAD_COLOR img = cv2.imread(str(filepath), flag) if img is None: print(f"⚠️ 图像无法解码: {filepath}") return img print(f"📁 正在从 {data_dir} 加载数据...") # --- 仅尝试加载 tsukuba 数据(修复GT加载为灰度图)--- tsukuba_img1 = load_image("tsukuba1.jpg") tsukuba_img2 = load_image("tsukuba2.jpg") tsukuba_gt = load_image("tsukuba_gt.jpg", grayscale=True) # 修正:GT应加载为灰度图 if tsukuba_img1 is None or tsukuba_img2 is None: print("🛑 至少缺少 tsukuba1.jpg 或 tsukuba2.jpg,请检查 data/ 目录!") return tsukuba_img1_gray = cv2.cvtColor(tsukuba_img1, cv2.COLOR_BGR2GRAY).astype(np.float32) tsukuba_img2_gray = cv2.cvtColor(tsukuba_img2, cv2.COLOR_BGR2GRAY).astype(np.float32) tsukuba_gt_gray = tsukuba_gt.astype(np.float32) if tsukuba_gt is not None else None if tsukuba_gt_gray is not None: print(f"✅ 成功加载图像: tsukuba1.jpg, tsukuba2.jpg, tsukuba_gt.jpg") else: print(f"✅ 成功加载图像: tsukuba1.jpg, tsukuba2.jpg(未找到 ground truth)") # ------------------------------- # Task 0: OpenCV StereoBM(推荐使用) # ------------------------------- if '0' in tasks: print('🔧 Running task0: OpenCV StereoBM...') stereo = cv2.StereoBM.create(numDisparities=16, blockSize=15) # 修正:视差范围匹配Tsukuba disp_raw = stereo.compute(tsukuba_img1_gray, tsukuba_img2_gray).astype(np.float32) disparity_cv2 = disp_raw / 16.0 # 解码 fixed-point disparity_cv2[disparity_cv2 <= 0] = 0 visualize_disparity_map( disparity_cv2, tsukuba_gt_gray, save_path="output/task0_tsukuba_cv2.png" ) if '2' in tasks: task2_visualize_pointcloud( tsukuba_img1, disparity_cv2, save_path='output/task2_tsukuba_cv2.ply' ) # ------------------------------- # Task 1: 简单匹配算法 # ------------------------------- if '1' in tasks: print('🔍 Running task1: Simple Matching...') maps = task1_simple_disparity(tsukuba_img1_gray, tsukuba_img2_gray, tsukuba_gt_gray, 'tsukuba') if '2' in tasks: for m, ws, mf, dr in maps: dmin, dmax = dr task2_visualize_pointcloud( tsukuba_img1, m, save_path=f'output/task2_tsukuba_{ws}_{dmin}_{dmax}_{mf}.ply' ) # ------------------------------- # Task 3: 动态规划 # ------------------------------- if '3' in tasks: print('🔄 Running task3: Dynamic Programming...') # 修正:规范打印信息 disparity_dp = task3_compute_disparity_map_dp(tsukuba_img1_gray, tsukuba_img2_gray) disparity_dp[disparity_dp <= 0] = 0 visualize_disparity_map( disparity_dp, tsukuba_gt_gray, save_path='output/task3_tsukuba_dp.png' ) if '2' in tasks: task2_visualize_pointcloud( tsukuba_img1, disparity_dp, save_path='output/task2_tsukuba_dp.ply' ) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Homework 4: Stereo Matching (Tsukuba Only)') parser.add_argument('--tasks', type=str, default='02', help='要运行的任务,例如 0, 02, 123') args = parser.parse_args() main(args.tasks)。
最新发布
11-28
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值