动态规划优化之使用Map记忆化搜索

已此题为例:

http://acm.hdu.edu.cn/showproblem.php?pid=1864


原来的解法:

http://blog.youkuaiyun.com/gaotong2055/article/details/8579388


上面的解法中DFS太耗时。就算是使用典型的动态规划,耗费空间,数组开的太大,有很多浪费,有太多的无用循环,也会耗时(200ms左右)。

这里使用map存数中间值,节省了空间和时间。



#include <stdio.h>
#include <string.h>
#include <iostream>
#include <map>
using namespace std;
double data[31];
int len,maxa;
double ans,q;



class OPT{
public:
	int k;
	double d;
	OPT(int a,double b):k(a),d(b){}
	OPT(){}
	bool operator == (const OPT & o) const{
		return k==o.k && d==o.d;
	}
	bool operator < (const OPT & o) const{
		if(k == o.k)
			return d < o.d;
		else
			return k < o.k;
	}
};

map<OPT,double> opts; //存储中间结果

double dfs(int k,double remain)
{
	OPT tmp(k, remain);
	if(opts[tmp] )
		return opts[tmp];
    if(k>=len)
    {
        return 0;
    }
    if(remain >= data[k])
    	return opts[tmp] = max(dfs(k+1,remain-data[k])+data[k], dfs(k+1,remain));
    else
    	return opts[tmp] = dfs(k+1,remain);
	return 0;
}

int main()
{
    int n,m;
    char cc;

    while(scanf("%lf %d",&q,&n)&&n!=0)
    {
        len=0;ans=-1;
        for(int i=0;i<n;i++)
        {
            scanf("%d",&m);
            double a=0,b=0,c=0,temp,sum=0;
            int flag=1;
            while(m--)
            {
                scanf(" %c:%lf",&cc,&temp);
                if(cc=='A') a+=temp;
                else if(cc=='B') b+=temp;
                else if(cc=='C') c+=temp;
                else    flag=0;
                sum+=temp;
            }
            if(flag&&sum<=1000&&a<=600&&b<=600&&c<=600)
                data[len++]= sum ;
        }
        opts.clear();
        ans =  dfs(0,q);
        printf("%.2f\n",ans);
    }
}


这是一个非常深刻的问题。 我们目前的解法是:**枚举初始长度 `L1`,然后贪心模拟扩展过程**。这种做法本质上是“暴力+启发式优化”。 现在你想知道: > ✅ 是否可以用 **动态规划(DP)或记忆化搜索** 来替代这个暴力模拟? --- ## 🔍 问题本质回顾 我们要在数组 `s[0..m-1]` 中构造一个序列 $b$,规则如下: - 初始选一段 `L1` 作为第一块 - 后续每一块长度必须 ≥ 当前 `|b|` - 若新块 `c` 满足 $\exists\, i \in [1,\min(|b|,|c|)]$ 使得 $b_i = c_i$,则得分 +1,并将 `c` 接到 `b` 后面 - 目标:最大化得分 注意:每次接上的块长度等于当前 `built`(即 `|b|`),这是最优策略中常见的选择(贪心取最小合法长度) --- ## ❌ 为什么不能直接用 DP? 因为状态空间巨大: ### 状态设计难点: - `dp[pos]` 表示从位置 `pos` 开始能得多少分? → 不行,缺少上下文 - `dp[built_len][last_pos]`?→ `built_len` 可达 $10^5$,状态爆炸 - 状态需要知道当前 `b` 序列的内容?→ 太大,无法存储 所以 **无法直接对“构造出的 b”建模** --- ## ✅ 正确方向:DP on 构造路径(基于贪心性质) 但我们观察到一个重要事实: > 在最优策略中,通常采用 **倍增式增长**: > > - 块长:`L1`, `L1`, `2*L1`, `4*L1`, ... > - 总长度增长近似指数级 ⇒ 最多 $O(\log m)$ 步 这说明:**整个构造过程最多只有 $O(\log m)$ 次扩展** 我们可以利用这一点,设计 **记忆化搜索(Memoized DFS)** 来替代暴力模拟。 --- ## ✅ 解法思路:以 `(start, len)` 为状态进行记忆化搜索 定义: ```cpp map<pair<int, int>, int> memo; // memo[{start, len}] = 从位置 start 开始,当前已构造序列长度为 len, // 最多还能得多少分 ``` 但这仍然太大 —— `start` 和 `len` 都可达 $10^5$ --- ## ✅ 更优思路:只记录“当前 built 长度”和“当前位置”,但利用贪心限制转移 实际上,在标准策略中: - 每次扩展都使用 `c_len = built` - 所以下一步的位置是确定的 因此,整个扩展路径由 `L1` 完全决定! 也就是说:**一旦选定 `L1`,后续路径唯一确定(贪心模拟即可)** ➡️ 这意味着:**不存在“多种选择”的分支结构**,DP 无用武之地! --- ## 🚫 结论一:不能用传统 DP 替代枚举 `L1` 因为: - 没有重叠子问题(每个 `L1` 走一条独立路径) - 状态无法压缩 - 决策是线性的、贪心的,不是多路选择 所以:**这不是一个典型的可 DP 化问题** --- ## ✅ 但是!我们可以用“记忆化剪枝”来加速多个查询中的重复模式 虽然单个查询内难以 DP,但在多组数据或多次查询中,可以尝试缓存某些结果。 ### 改进方向 1:记忆化“某个起始位置 + 长度”是否能匹配 我们发现内层循环频繁判断: > 给定 `built`, `c_len`,是否存在 `k < c_len` 使得 `s[k] == s[built + k]` 这个可以预处理成一张表吗? #### ✅ 方法:构建二维布尔数组 `can_match[i][j]` 表示 `s[i] == s[j]` 但不行,我们需要的是:是否存在 `k < L` 使得 `s[k] == s[pos + k]` 这其实是字符串匹配中的“对齐相等性”问题。 --- ## ✅ 实用技巧:预计算所有偏移量下的“对齐匹配”快速查询 定义: ```cpp vector<unordered_set<int>> pos_of_value(51); // 值 v 出现在哪些位置 ``` 然后对于给定的 `built` 和 `c_len`,我们想快速判断: > 存在 `k ∈ [0, c_len)` 使得 `s[k] == s[built + k]` 等价于:存在某个值 `v ∈ [0,50]`,使得 `v` 同时出现在 `s[0:c_len]` 和 `s[built: built+c_len]` 的相同偏移 `k` 上 我们可以这样做: ```cpp bool check_match(const vector<int>& s, int built, int c_len, const vector<vector<int>>& pos) { for (int v = 0; v <= 50; ++v) { const auto& vec = pos[v]; // 二分查找:是否有 k < c_len 且 built + k 是 v 的出现位置? for (int k : vec) { if (k >= c_len) break; if (built + k < s.size() && s[built + k] == v) { return true; } } } return false; } ``` 但这仍是 $O(\text{occ})$ --- ## ✅ 最佳折衷方案:结合贪心路径 + 记忆化得分路径 既然路径由 `L1` 决定,我们可以: ### 定义函数: ```cpp int simulate(int L1, const vector<int>& s) ``` 返回以 `L1` 开头最多得几分。 我们可以对 `(s_hash, L1)` 缓存结果,避免重复计算相同子数组。 --- ## ✅ 完整记忆化版本代码(支持子数组哈希缓存) ```cpp #include <bits/stdc++.h> using namespace std; // 子数组哈希函数(简单多项式哈希) long long hash_subarray(const vector<int>& s, int l, int r) { long long h = 0; for (int i = l; i <= r; ++i) { h = h * 131LL + s[i]; } return h; } // 全局缓存:hash -> map<L1, score> map<long long, map<int, int>> cache; int simulate(int L1, const vector<int>& s) { int m = s.size(); if (L1 >= m) return 0; int built = L1; int score = 0; while (built < m) { int c_len = built; if (built + c_len > m) break; bool match = false; int limit = min(c_len, 8); for (int k = 0; k < limit; ++k) { if (s[k] == s[built + k]) { match = true; break; } } if (!match && c_len > limit) { for (int k = limit; k < c_len; ++k) { if (s[k] == s[built + k]) { match = true; break; } } } if (!match) break; score++; built += c_len; } return score; } int solve_query(const vector<int>& s) { int m = s.size(); if (m <= 1) return 0; long long h = hash_subarray(s, 0, m - 1); auto& cached_L1 = cache[h]; int best = 0; for (int L1 = 1; L1 < m && L1 <= 64; ++L1) { // 限制 L1 范围 if (cached_L1.count(L1)) { best = max(best, cached_L1[L1]); } else { int res = simulate(L1, s); cached_L1[L1] = res; best = max(best, res); } } return best; } ``` --- ## ✅ 使用方式 ```cpp int main() { int n, q; scanf("%d%d", &n, &q); vector<int> a(n); for (int i = 0; i < n; ++i) { scanf("%d", &a[i]); } while (q--) { int l, r; scanf("%d%d", &l, &r); l--; r--; vector<int> s(a.begin() + l, a.begin() + r + 1); printf("%d\n", solve_query(s)); } return 0; } ``` --- ## ✅ 优势分析 | 特性 | 说明 | |------|------| | ✅ 时间优化 | 对相同/相似子数组避免重复计算 | | ✅ 空间可控 | 只缓存实际出现的子数组哈希 | | ✅ 易于扩展 | 可加入更多启发式(如跳过不可能的 L1) | | ⚠️ 局限性 | 若所有子数组不同,缓存无效 | --- ## ✅ 总结回答 > **能否用动态规划记忆化搜索替代暴力模拟?** ### ❌ 不能使用传统 DP: - 没有重叠子问题 - 状态空间过大(依赖完整历史) - 路径由 `L1` 决定,无分支 ### ✅ 但可以使用记忆化模拟”: - 缓存 `(子数组哈希, L1)` → 得分 - 利用贪心路径唯一性减少重复计算 - 结合值域小、早期匹配快等特性提升效率 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值