【数据结构】动态规划(Dynamic Programming)-CSDN博客 https://blog.csdn.net/Hsianus/article/details/134802356?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-134802356.142^v100^pc_search_result_base2&spm=1018.2226.3001.4187
【大前言】
个人认为贪心,dp�� 是最难的,每次遇到题完全不知道该怎么办,看了题解后又瞬间恍然大悟(TAT)。这篇文章也是花了我差不多一个月时间才全部完成。
【进入正题】
用动态规划解决问题具有空间耗费大、时间效率高的特点,但也会有时间效率不能满足要求的时候,如果算法有可以优化的余地,就可以考虑时间效率的优化。
【DP 时间复杂度的分析】
DP�� 高时间效率的关键在于它减少了“冗余”,即不必要的计算或重复计算部分,算法的冗余程度是决定算法效率的关键。而动态规划就是在将问题规模不断缩小的同时,记录已经求解过的子问题的解,充分利用求解结果,避免了反复求解同一子问题的现象,从而减少“冗余”。
但是,一个动态规划问题很难做到完全消除“冗余”。
下面给出动态规划时间复杂度的决定因素:
时间复杂度 == 状态总数 ×× 每个状态转移的状态数 ×× 每次状态转移的时间
【DP 优化思路】
一:减少状态总数
(1).(1). 改进状态表示
(2).(2).选择适当的规划方向
二:减少每个状态转移的状态数
(1).(1). 四边形不等式和决策的单调性
(2).(2). 决策量的优化
(3).(3). 合理组织状态
(4).(4). 细化状态转移
三:减少状态转移的时间
(1).(1). 减少决策时间
(2).(2). 减少计算递推式的时间
(上述内容摘自 《动态规划算法的优化技巧》毛子青 ,想要深入了解其思想的可以去看看这篇写得超级好的论文。)
看到这里是不是已经感觉有点蒙了呢?
本蒟蒻总结了一个简化版本:
【DP 三要点】
在推导 dp�� 方程时,我们时常会感到毫无头绪,而实际上 dp�� 方程也是有迹可循的,总的来说,需要关注两个要点:状态,决策和转移。其中 “状态” 又最为关键,决策最为复杂。
【状态】
关于 “状态” 的优化可以从很多角度出发,思维难度及其高,有时候状态选择的好坏会直接导致出现暴零和满分的分化。
【决策】
与 “状态” 不同,“决策” 优化则有着大量模板化的东西,在各大书籍,文章上你都可以看到这样的话:只要是形如 XXX��� 的状态转移方程,都可以用 XXX��� 进行优化。
【转移】
“转移” 则指由最优决策点得到答案的转移过程,其复杂度一般较低,通常可以忽略,但有时也需要特别注意并作优化。
本文将会重点针对 “决策” 优化部分作一些总结,记录自己的感悟和理解。
QAQ���
一:【矩阵优化 DP】
updata������ 之后由于篇幅过大,已搬出。。。。。
补充:其实质是优化 “转移”。
QAQ���
二:【数据结构优化 DP】
【前言】
在一些 dp�� 方程的状态转移过程中,我们通常需要在某个范围内进行择优,选出最佳决策点,这往往可以作为 dp�� 优化的突破口。
数据结构的使用较灵活,没有一个特定的模板,需要根据具体情况而定,选择合适的方案。由于状态转移总是伴随着区间查询最值,区间求和等操作,即动态区间操作,所以平衡树可以作为一个有用的工具,但考虑到代码复杂度,使用树状数组或者线段树将会是一个不错的选择。。
其实质是优化 “决策”。
1.【维护合适的信息】
以 The�ℎ� Battle������ of�� Chibi�ℎ��� [UVA12983][���12983] 为例,大概题意就是计算在给定的序列中严格单调递增子序列的数量,并对 1e9+71�9+7 取模,给定序列长度小于等于 10001000 。
方程应该是比较好推的,用 dp[i][j]��[�][�] 表示由序列中在 j� 之前的数构成并以 a[j]�[�] 结尾的子序列中,长度为 i� 的子序列的数量。则:dp[i][j]=∑dp[i−1][k]��[�][�]=∑��[�−1][�] ,其中 k<j�<� 且且 a[k]<a[j]�[�]<�[�] 。
对于决策点 dp[i−1][k]��[�−1][�] 这里出现了 33 个信息:
(1).(1). 在原序列中的位置 k<j�<� 。
(2).(2). a[k]<a[j]�[�]<�[�] 。
(3).(3). dp[i−1][k]��[�−1][�] 的和。
对于 (1)(1),可以用枚举的顺序解决,剩下的两个信息即是数据结构需要维护的信息。
对于每一次 dp[i]��[�] 的决策,可以将 a[j]�[�] 作为数据结构维护的关键字, dp[i−1][j]��[�−1][�] 作为权值,加入 −inf−��� 后离散化,得到一个大小为 N+1�+1 的数组并在上面建立树状数组,每次计算 dp[i][j]��[�][�] 时查询前面已经加入的且关键字小于 a[j]�[�] 的 dp[i−1][k]��[�−1][�] 总和(即区间查询),然后把 dp[i−1][j]��[�−1][�] 加入树状数组(单点查询)。
时间复杂度为 O(logn)�(����)。
当问题涉及到的操作更复杂时,树状数组无法维护所需要的信息,就只有用线段树了。这道题较简单,所以选择了代码复杂度更低的树状数组。
2.【Code】
#include<algorithm>
#include<cstring>
#include<cstdlib>//UVA抽风,加上这个就好了
#include<cstdio>
#define Re register int
using namespace std;//UVA抽风,还要加这个
const int N=1005,P=1e9+7;
int n,m,T,k,ans,cnt,a[N],b[N],C[N],dp[N][N];
inline void add(Re x,Re v){while(x<=n+1)(C[x]+=v)%=P,x+=x&-x;}
inline int ask(Re x){Re ans=0;while(x)(ans+=C[x])%=P,x-=x&-x;return ans%P;}
int main(){
scanf("%d",&T);
for(Re o=1;o<=T;++o){
scanf("%d%d",&n,&m),ans=0,cnt=n;
memset(dp,0,sizeof(dp));
for(Re i=1;i<=n;++i)scanf("%d",&a[i]),b[i]=a[i],dp[1][i]=1;
sort(b+1,b+n+1);//离散
cnt=unique(b+1,b+n+1)-b-1;//去重
for(Re i=2;i<=m;++i){
memset(C,0,sizeof(C));//每次都要清空,重新开始维护
for(Re j=1;j<=n;++j)
dp[i][j]=ask((k=lower_bound(b+1,b+cnt+1,a[j])-b)-1),add(k,dp[i-1][j]);
}
for(Re j=1;j<=n;++j)(ans+=dp[m][j])%=P;
printf("Case #%d: %d\n",o,ans);
}
}
3.【题目链接】
【简单题】
- The�ℎ� Battle������ of�� Chibi�ℎ��� [UVA12983][���12983]
【 标签】动态规划/树状数组
【高档题】
-
方伯伯的玉米田 [P3287][�3287]
【 标签】动态规划/二维树状数组