转转来 發表於 2019-8-7 19:36:00

【学习笔记】动态规划—各种 DP 优化

<h1 id="学习笔记动态规划各种-dp-优化"><strong>【学习笔记】动态规划—各种 DP 优化</strong></h1>
<h2 id="大前言"><strong>【大前言】</strong></h2>
<p>个人认为贪心,<span class="math inline">\(dp\)</span> 是最难的,每次遇到题完全不知道该怎么办,看了题解后又瞬间恍然大悟(TAT)。这篇文章也是花了我差不多一个月时间才全部完成。</p>
<hr>
<h3 id="进入正题"><strong>【进入正题】</strong></h3>
<p>用动态规划解决问题具有<strong>空间耗费大</strong>、<strong>时间效率高</strong>的特点,但也会有时间效率不能满足要求的时候,如果算法有可以优化的余地,就可以考虑时间效率的优化。</p>
<hr>
<h3 id="dp-时间复杂度的分析"><strong>【DP 时间复杂度的分析】</strong></h3>
<p><span class="math inline">\(DP\)</span> 高时间效率的关键在于它减少了“<strong>冗余</strong>”,即不必要的计算或重复计算部分,算法的冗余程度是决定算法效率的关键。而动态规划就是在将问题规模不断缩小的同时,记录已经求解过的子问题的解,充分利用求解结果,避免了反复求解同一子问题的现象,从而减少“<strong>冗余</strong>”。<br>
但是,一个动态规划问题很难做到完全消除“<strong>冗余</strong>”。</p>
<p>下面给出动态规划时间复杂度的决定因素:</p>
<p><strong>时间复杂度 <span class="math inline">\(=\)</span> 状态总数 <span class="math inline">\(×\)</span> 每个状态转移的状态数 <span class="math inline">\(×\)</span> 每次状态转移的时间</strong></p>
<hr>
<h3 id="dp-优化思路"><strong>【DP 优化思路】</strong></h3>
<h4 id="一减少状态总数"><strong>一:减少状态总数</strong></h4>
<p><span class="math inline">\((1).\)</span> <strong>改进状态表示</strong></p>
<p><span class="math inline">\((2).\)</span><strong>选择适当的规划方向</strong></p>
<h4 id="二减少每个状态转移的状态数"><strong>二:减少每个状态转移的状态数</strong></h4>
<p><span class="math inline">\((1).\)</span> <strong>四边形不等式和决策的单调性</strong></p>
<p><span class="math inline">\((2).\)</span> <strong>决策量的优化</strong></p>
<p><span class="math inline">\((3).\)</span> <strong>合理组织状态</strong></p>
<p><span class="math inline">\((4).\)</span> <strong>细化状态转移</strong></p>
<h4 id="三减少状态转移的时间"><strong>三:减少状态转移的时间</strong></h4>
<p><span class="math inline">\((1).\)</span> <strong>减少决策时间</strong></p>
<p><span class="math inline">\((2).\)</span> <strong>减少计算递推式的时间</strong></p>
<p>(上述内容摘自 <em>《动态规划算法的优化技巧》毛子青</em> ,想要深入了解其思想的可以去看看这篇写得超级好的论文。)</p>
<hr>
<p>看到这里是不是已经感觉有点蒙了呢?<br>
本蒟蒻总结了一个简化版本:</p>
<hr>
<h3 id="dp-三要点"><strong>【DP 三要点】</strong></h3>
<p>在推导 <span class="math inline">\(dp\)</span> 方程时,我们时常会感到毫无头绪,而实际上 <span class="math inline">\(dp\)</span> 方程也是有迹可循的,总的来说,需要关注两个要点:<strong>状态</strong>,<strong>决策</strong>和<strong>转移</strong>。其中 <strong>“状态”</strong> 又最为关键,决策最为复杂。</p>
<h4 id="状态"><strong>【状态】</strong></h4>
<p>关于 <strong>“状态”</strong> 的优化可以从很多角度出发,思维难度及其高,有时候状态选择的好坏会直接导致出现暴零和满分的分化。</p>
<h4 id="决策"><strong>【决策】</strong></h4>
<p>与 <strong>“状态”</strong> 不同,<strong>“决策”</strong> 优化则有着大量模板化的东西,在各大书籍,文章上你都可以看到这样的话:只要是形如 <span class="math inline">\(XXX\)</span> 的状态转移方程,都可以用 <span class="math inline">\(XXX\)</span> 进行优化。</p>
<h4 id="转移"><strong>【转移】</strong></h4>
<p><strong>“转移”</strong> 则指由最优决策点得到答案的转移过程,其复杂度一般较低,通常可以忽略,但有时也需要特别注意并作优化。</p>
<p>本文将会重点针对 <strong>“决策”</strong> 优化部分作一些总结,记录自己的感悟和理解。</p>
<hr>
<p></p><div class="math display">\[QAQ
\]</div><p></p><hr>
<h2 id="一矩阵优化-dp"><strong>一:【矩阵优化 DP】</strong></h2>
<p><span class="math inline">\(updata\)</span> 之后由于篇幅过大,已搬出。。。。。</p>
<p>【学习笔记】动态规划—矩阵递推加速</p>
<p>补充:其实质是优化 <strong>“转移”</strong>。</p>
<hr>
<p></p><div class="math display">\[QAQ
\]</div><p></p><hr>
<h2 id="二数据结构优化-dp"><strong>二:【数据结构优化 DP】</strong></h2>
<h3 id="前言"><strong>【前言】</strong></h3>
<p>在一些 <span class="math inline">\(dp\)</span> 方程的状态转移过程中,我们通常需要在某个范围内进行择优,选出最佳决策点,这往往可以作为 <span class="math inline">\(dp\)</span> 优化的突破口。</p>
<p>数据结构的使用较灵活,没有一个特定的模板,需要根据具体情况而定,选择合适的方案。由于状态转移总是伴随着区间查询最值,区间求和等操作,即<strong>动态区间操作</strong>,所以<strong>平衡树</strong>可以作为一个有用的工具,但考虑到代码复杂度,使用<strong>树状数组</strong>或者<strong>线段树</strong>将会是一个不错的选择。。</p>
<p>其实质是优化 <strong>“决策”</strong>。</p>
<hr>
<h3 id="1维护合适的信息"><strong>1.【维护合适的信息】</strong></h3>
<p>以 <span class="math inline">\(The\)</span> <span class="math inline">\(Battle\)</span> <span class="math inline">\(of\)</span> <span class="math inline">\(Chibi\)</span> <span class="math inline">\(\)</span> 为例,大概题意就是计算在给定的序列中严格单调递增子序列的数量,并对 <span class="math inline">\(1e9
+7\)</span> 取模,给定序列长度小于等于 <span class="math inline">\(1000\)</span> 。</p>
<p>方程应该是比较好推的,用 <span class="math inline">\(dp\)</span> 表示由序列中在 <span class="math inline">\(j\)</span> 之前的数构成并以 <span class="math inline">\(a\)</span> 结尾的子序列中,长度为 <span class="math inline">\(i\)</span> 的子序列的数量。则:<span class="math inline">\(dp=\sum dp\)</span> ,其中 <span class="math inline">\(k &lt; j\)</span> <span class="math inline">\(且\)</span> <span class="math inline">\(a &lt; a\)</span> 。</p>
<p>对于决策点 <span class="math inline">\(dp\)</span> 这里出现了 <span class="math inline">\(3\)</span> 个信息:<br>
<span class="math inline">\((1).\)</span> 在原序列中的位置 <span class="math inline">\(k&lt;j\)</span> 。<br>
<span class="math inline">\((2).\)</span> <span class="math inline">\(a&lt;a\)</span> 。<br>
<span class="math inline">\((3).\)</span> <span class="math inline">\(dp\)</span> 的和。</p>
<p>对于 <span class="math inline">\((1)\)</span>,可以用枚举的顺序解决,剩下的两个信息即是数据结构需要维护的信息。</p>
<p>对于每一次 <span class="math inline">\(dp\)</span> 的决策,可以将 <span class="math inline">\(a\)</span> 作为数据结构维护的关键字, <span class="math inline">\(dp\)</span> 作为权值,加入 <span class="math inline">\(-inf\)</span> 后离散化,得到一个大小为 <span class="math inline">\(N+1\)</span> 的数组并在上面建立树状数组,每次计算 <span class="math inline">\(dp\)</span> 时查询前面已经加入的且关键字小于 <span class="math inline">\(a\)</span> 的 <span class="math inline">\(dp\)</span> 总和(即区间查询),然后把 <span class="math inline">\(dp\)</span> 加入树状数组(单点查询)。</p>
<p><strong>时间复杂度为</strong> <span class="math inline">\(O(logn)\)</span>。</p>
<p>当问题涉及到的操作更复杂时,树状数组无法维护所需要的信息,就只有用线段树了。这道题较简单,所以选择了代码复杂度更低的树状数组。</p>
<hr>
<h3 id="2code"><strong>2.【Code】</strong></h3>
<pre><code class="language-cpp">#include&lt;algorithm&gt;
#include&lt;cstring&gt;
#include&lt;cstdlib&gt;//UVA抽风,加上这个就好了
#include&lt;cstdio&gt;
#define Re register int
using namespace std;//UVA抽风,还要加这个
const int N=1005,P=1e9+7;
int n,m,T,k,ans,cnt,a,b,C,dp;
inline void add(Re x,Re v){while(x&lt;=n+1)(C+=v)%=P,x+=x&amp;-x;}
inline int ask(Re x){Re ans=0;while(x)(ans+=C)%=P,x-=x&amp;-x;return ans%P;}
int main(){
    scanf("%d",&amp;T);
    for(Re o=1;o&lt;=T;++o){
      scanf("%d%d",&amp;n,&amp;m),ans=0,cnt=n;
      memset(dp,0,sizeof(dp));
      for(Re i=1;i&lt;=n;++i)scanf("%d",&amp;a),b=a,dp=1;
      sort(b+1,b+n+1);//离散
      cnt=unique(b+1,b+n+1)-b-1;//去重
      for(Re i=2;i&lt;=m;++i){
            memset(C,0,sizeof(C));//每次都要清空,重新开始维护
            for(Re j=1;j&lt;=n;++j)
                dp=ask((k=lower_bound(b+1,b+cnt+1,a)-b)-1),add(k,dp);
      }       
      for(Re j=1;j&lt;=n;++j)(ans+=dp)%=P;
      printf("Case #%d: %d\n",o,ans);
    }
}
</code></pre>
<hr>
<h3 id="3题目链接"><strong>3.【题目链接】</strong></h3>
<h4 id="简单题"><strong>【简单题】</strong></h4>
<ul>
<li><span class="math inline">\(The\)</span> <span class="math inline">\(Battle\)</span> <span class="math inline">\(of\)</span> <span class="math inline">\(Chibi\)</span> <span class="math inline">\(\)</span><br>
【 标签】动态规划/树状数组</li>
</ul>
<h4 id="高档题"><strong>【高档题】</strong></h4>
<ul>
<li>
<p>方伯伯的玉米田 <span class="math inline">\(\)</span><br>
【 标签】动态规划/二维树状数组</p>
</li>
<li>
<p>基站选址 <span class="math inline">\(\)</span><br>
【 标签】动态规划/线段树</p>
</li>
</ul>
<hr>
<p></p><div class="math display">\[QAQ
\]</div><p></p><hr>
<h2 id="三决策单调性"><strong>三:【决策单调性】</strong></h2>
<h3 id="前言-1"><strong>【前言】</strong></h3>
<p>形如 <span class="math inline">\(dp=min(dp+w(j,i))\)</span> <span class="math inline">\((L_i \leqslant j \leqslant R_i)\)</span> 的 <span class="math inline">\(dp\)</span> 方程被称作 <span class="math inline">\(1D/1D\)</span> 动态规划,其中 <span class="math inline">\(L_i\)</span> 和 <span class="math inline">\(R_i\)</span> 单调递增,<span class="math inline">\(w(j,i)\)</span> 决定着优化策略选择。</p>
<p>针对决策点具有的特性,可以大大降低寻找最优决策点的时间。</p>
<p>其实质是优化 <strong>“决策”</strong>。</p>
<hr>
<h3 id="1定义"><strong>1.【定义】</strong></h3>
<h4 id="决策单调性"><strong>【决策单调性】</strong></h4>
<p>设 <span class="math inline">\(j_0\)</span> 表示 <span class="math inline">\(dp\)</span> 转移的<strong>最优决策点</strong>,那么<strong>决策单调性</strong>可描述为 <span class="math inline">\(\forall i \leqslant j,j_0 \leqslant j_0\)</span>。也就是说随着 <span class="math inline">\(i\)</span> 的增大,所找到的<strong>最优决策点</strong>是递增态(非严格递增)。</p>
<h4 id="四边形不等式"><strong>【四边形不等式】</strong></h4>
<p><span class="math inline">\(w(x,y)\)</span> 为定义在整数集合上的一个二元函数,若 <span class="math inline">\(\forall a \leqslant b \leqslant c \leqslant d,w(a,c)+w(b,d) \leqslant w(a,d)+w(b,c)\)</span>,那么函数 <span class="math inline">\(w\)</span> 满足<strong>四边形不等式</strong>。</p>
<p>为什么叫做<strong>四边形不等式</strong>呢?在 <span class="math inline">\(w(x,y)\)</span> 函数的二维矩阵中挖一块四边形,<strong>左上角</strong> 加 <strong>右下角</strong> 小于等于 <strong>左下角</strong> 加 <strong>右上角</strong>。</p>
<hr>
<h3 id="2定理及其证明"><strong>2.【定理及其证明】</strong></h3>
<h4 id="定理-1四边形不等式的另一种定义"><strong>定理 (1):四边形不等式的另一种定义</strong></h4>
<p><span class="math inline">\(w(x,y)\)</span> 为定义在整数集合上的一个二元函数,若 <span class="math inline">\(\forall a &lt; b,w(a,b)+w(a+1,b+1) \leqslant w(a+1,b)+w(a,b+1)\)</span>,那么函数 <span class="math inline">\(w\)</span> 满足<strong>四边形不等式</strong>。</p>
<p><span class="math inline">\(证明:\)</span></p>
<p><span class="math inline">\(\because \forall a &lt; c,w(a,c)+w(a+1,c+1) \leqslant w(a+1,c)+w(a,c+1)\)</span></p>
<p><span class="math inline">\(\therefore \forall a+1 &lt; c,w(a+1,c)+w(a+2,c+1) \leqslant w(a+2,c)+w(a+1,c+1)\)</span></p>
<p><span class="math inline">\(上下两式相加,有:\)</span> <span class="math inline">\(w(a,c)+w(a+2,c+1) \leqslant w(a,c+1)+w(a+2,c)\)</span></p>
<p><span class="math inline">\(以此类推\)</span> <span class="math inline">\(\forall a \leqslant b\leqslant c,w(a,c)+w(b,c+1) \leqslant w(a,c+1)+w(b,c)\)</span></p>
<p><span class="math inline">\(同理\)</span> <span class="math inline">\(\forall a \leqslant b \leqslant c \leqslant d,w(a,c)+w(b,d) \leqslant w(a,d)+w(b,c)\)</span></p>
<h4 id="定理-2决策单调性"><strong>定理 (2):决策单调性</strong></h4>
<p><span class="math inline">\(1D/1D\)</span> 动态规划具有<strong>决策单调性</strong>当且仅当函数 <span class="math inline">\(w\)</span> 满足<strong>四边形不等式</strong> 时成立。</p>
<p><span class="math inline">\(证明:\)</span></p>
<p><span class="math inline">\(\because\)</span> <span class="math inline">\(j_0\)</span> <span class="math inline">\(在\)</span> <span class="math inline">\(dp\)</span> <span class="math inline">\(的决策点中最优\)</span><br>
<span class="math inline">\(\therefore\)</span> <span class="math inline">\(\forall i \in ,\forall j \in -1],dp]+w(j_0,i) \leqslant dp+w(j,i)\)</span></p>
<p><span class="math inline">\(易知\)</span> <span class="math inline">\(\forall i' \in \)</span>,<span class="math inline">\(均满足\)</span> <span class="math inline">\(j&lt;j_0&lt;i&lt;i'\)</span>。<br>
<span class="math inline">\(又\)</span> <span class="math inline">\(\because\)</span> <span class="math inline">\(函数\)</span> <span class="math inline">\(w\)</span> <span class="math inline">\(满足四边形不等式\)</span><br>
<span class="math inline">\(\therefore\)</span> <span class="math inline">\(w(j,i)+w(j_0,i') \leqslant w(j,i')+w(j_0,i)\)</span></p>
<p><span class="math inline">\(移项得:\)</span> <span class="math inline">\(w(j_0,i')-w(j_0,i) \leqslant w(j,i')-w(j,i)\)</span></p>
<p><span class="math inline">\(与第一个式子相加,有:\)</span> <span class="math inline">\(dp]+w(j_0,i') \leqslant dp+w(j,i')\)</span></p>
<p>最后的式子含义是:把 <span class="math inline">\(j_0\)</span> 作为 <span class="math inline">\(dp\)</span> 的决策点,一定比小于 <span class="math inline">\(j_0\)</span> 的任意一个 <span class="math inline">\(j\)</span> 都要更好。也就是说,<span class="math inline">\(dp\)</span> 的<strong>最优决策点</strong>不可能小于 <span class="math inline">\(j_0\)</span> ,即 <span class="math inline">\(j_0 \geqslant j_0\)</span>,所以方程 <span class="math inline">\(dp\)</span> 具有<strong>决策单调性</strong>。</p>
<hr>
<h3 id="3证明决策单调性"><strong>3.【证明决策单调性】</strong></h3>
<p>这里以 玩具装箱 <span class="math inline">\(toy\)</span> <span class="math inline">\(\)</span> 为例(因为这个比较好证 QAQ),先来证一波<strong>决策单调性</strong>。</p>
<p>设 <span class="math inline">\(S=\sum _{i=1}^n (C+1)\)</span>,用 <span class="math inline">\(dp\)</span> 表示装好前 <span class="math inline">\(i\)</span> 个的最小花费,则 <span class="math inline">\(dp\)</span> 方程为:<span class="math inline">\(dp=min(dp+(S-S-1-L)^2)\)</span>。</p>
<p>很明显,这个方程是一个 <span class="math inline">\(1D/1D\)</span> 动态规划方程,其中 <span class="math inline">\(w(i,j)=(S-S-1-L)^2\)</span>。</p>
<p>(注意在<strong>四边形不等式</strong>中的 <span class="math inline">\(j\)</span> 不是 <span class="math inline">\(i\)</span> 决策点,可以理解为 <span class="math inline">\(i’\)</span>。而 <span class="math inline">\(w(i,j)\)</span> 的值可以理解为是由已完成的状态 <span class="math inline">\(dp\)</span> 转移到 <span class="math inline">\(dp\)</span> 所带有的附加价值)。</p>
<p><span class="math inline">\(证明:设\)</span> <span class="math inline">\(Q=S-S-1-L\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)=(S-S-1-L)^2=Q^2\)</span></p>
<p><span class="math inline">\(\text{证明:设}\)</span> <span class="math inline">\(Q=S-S-1-L\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)=(S-S-1-L)^2=Q^2\)</span></p>
<p><span class="math inline">\(\begin{aligned}
\therefore w(i+1,j+1)=&amp;(S-S-1-L)^2\\
                   =&amp;((S+C+1)-(S+C+1)-1-L)^2\\
                   =&amp;(Q+C-C)^2
\end{aligned}\)</span></p>
<p><span class="math inline">\(\begin{aligned}
w(i,j+1)=&amp;(S-S-1-L)^2\\
                   =&amp;(S-(S+C+1)-1-L)^2\\
                   =&amp;(Q-C-1)^2
\end{aligned}\)</span></p>
<p><span class="math inline">\(\begin{aligned}
w(i+1,j)=&amp;(S-S-1-L)^2\\
                   =&amp;((S+C+1)-S-1-L)^2\\
                   =&amp;(Q+C+1)^2
\end{aligned}\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)+w(i+1,j+1)=2X^2+2CX-2CX+C^2-2CC+C^2\)</span></p>
<p><span class="math inline">\(\therefore w(i+1,j)+w(i,j+1)=2X^2+2CX-2CX+C^2+2C+2C+C^2+2\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)+w(i+1,j+1)-w(i+1,j)+w(i,j+1)=-2(C+1)(C+1)\)</span></p>
<p><span class="math inline">\(\text{又} \because C,C \geqslant 1\)</span></p>
<p><span class="math inline">\(\therefore -2(C+1)(C+1) \leqslant -8\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)+w(i+1,j+1) \leqslant w(i+1,j)+w(i,j+1)\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)+w(i+1,j+1)=2X^2+2CX-2CX+C^2-2CC+C^2\)</span></p>
<p><span class="math inline">\(\therefore w(i+1,j)+w(i,j+1)=2X^2+2CX-2CX+C^2+2C+2C+C^2+2\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)+w(i+1,j+1)-w(i+1,j)+w(i,j+1)=-2(C+1)(C+1)\)</span></p>
<p><span class="math inline">\(又 \because C,C \geqslant 1\)</span></p>
<p><span class="math inline">\(\therefore -2(C+1)(C+1) \leqslant -8\)</span></p>
<p><span class="math inline">\(\therefore w(i,j)+w(i+1,j+1) \leqslant w(i+1,j)+w(i,j+1)\)</span></p>
<p>由定理 <span class="math inline">\((1)\)</span> 可知,函数 <span class="math inline">\(w\)</span> 满足<strong>四边形不等式</strong>。<br>
又由定理 <span class="math inline">\((2)\)</span>可知, 方程 <span class="math inline">\(dp\)</span> 具有<strong>决策单调性</strong>。</p>
<p>在实战中,通常使用打表的形式来验证 <span class="math inline">\(w\)</span> 函数的递变规律。</p>
<hr>
<h3 id="4实现方法"><strong>4.【实现方法】</strong></h3>
<p>(<span class="math inline">\(ps.\)</span> 对于此处及以下语言不严谨处,大家可以认真思考并给予建议,待日后对其理解加深后再行修改。)</p>
<p>这里选择不同的例题将二者分开讲。</p>
<h4 id="二分栈"><strong>【二分栈】</strong></h4>
<p>二分栈,顾名思义就是二分加栈。</p>
<p>用栈维护单调的决策点,二分找到最优决策点。</p>
<p>以 <span class="math inline">\(Lightning\)</span> <span class="math inline">\(Conductor\)</span> <span class="math inline">\(\)</span> 为例,题目大意就是在给定长度为 <span class="math inline">\(n\)</span> 的序列 <span class="math inline">\(a\)</span> 中,对于每一个 <span class="math inline">\(i\)</span>,找到最小的自然数 <span class="math inline">\(p_i\)</span> 满足对于任意的 <span class="math inline">\(j \in \)</span>,均有 <span class="math inline">\(a_j \leqslant a_i+p_i-\sqrt{\left|i-j\right|}\)</span> 。</p>
<p>把这个式子变下形:<span class="math inline">\(p_i \geqslant a_j-a_i+\sqrt{\left|i-j\right|}\)</span> 。<br>
即 <span class="math inline">\(p_i=max \{ a_j+\sqrt{\left|i-j\right|} \} -a_i\)</span><br>
即 <span class="math inline">\(p_i = max \{ max\{a_j+\sqrt{i-j}\}(j \in ),max\{a_j+\sqrt{j-i}\}(j \in ) \}-a_i\)</span></p>
<p>可以发现里面两个 <span class="math inline">\(max\)</span> 可以视为同一个问题(只要把序列翻转一下就可以了),所以只需要考虑求出对于每一个 <span class="math inline">\(i\)</span> 的 <span class="math inline">\(max\{ a_j+\sqrt{i-j} \}\)</span>,其中 <span class="math inline">\(j \in \)</span>。</p>
<p>设 <span class="math inline">\(y_j=a_j+\sqrt{i-j}\)</span></p>
<p>那么我们会得到 <span class="math inline">\(n\)</span> 个关于 <span class="math inline">\(i\)</span> 函数,<span class="math inline">\(p_i=max\{ y_j \}\)</span>。</p>
<p>将样例画出来,如图:<br>
<img src="https://images.cnblogs.com/cnblogs_com/Xing-Ling/1457207/o_bdf.PNG" alt="" loading="lazy"></p>
<p>可知当 <span class="math inline">\(i=4\)</span> 时,直线 <span class="math inline">\(x=4\)</span> 与 <span class="math inline">\(y1=a_1+\sqrt{x-1}\)</span> 的交点即为 <span class="math inline">\(p_4\)</span>。</p>
<p>在上图中,对于任意 <span class="math inline">\(j \in \)</span> ,<span class="math inline">\(y1\)</span> 总是在最上面,也就是说下面的其他函数可以踢掉不要,但由于 <span class="math inline">\(sqrt\)</span> 函数的增速递减,会出现如图中 <span class="math inline">\(y2,y4\)</span> 的情况,即存在一个交点使得在该点两边时两条直线的位置关系不同。此时如果没有上面的 <span class="math inline">\(y1\)</span>,<span class="math inline">\(y2,y4\)</span> 都有可能成为答案,所以不能乱踢。</p>
<p>看下面这种情况:<br>
<img src="https://images.cnblogs.com/cnblogs_com/Xing-Ling/1457207/o_nsdbs.PNG" alt="" loading="lazy"></p>
<p>设 <span class="math inline">\(k_1\)</span> 为 <span class="math inline">\(y_1,y_2\)</span> 的交点,<span class="math inline">\(k_2\)</span> 为 <span class="math inline">\(y_2,y_3\)</span> 的交点。<br>
此时 <span class="math inline">\(k_1 &gt; k_2\)</span>,可以发现 <span class="math inline">\(y_2\)</span> 始终在其他直线的下面,可以放心的将其踢掉。</p>
<p>所以维护出来的决策集合大概就是酱紫的:<br>
<img src="https://images.cnblogs.com/cnblogs_com/Xing-Ling/1457207/o_hydtuydu.PNG" alt="" loading="lazy"></p>
<p>对于不同的 <span class="math inline">\(i\)</span> 来说,都有一个互不不同的直线在最上方,所以该决策集合里的直线都是有用的。可以从图中看出,最优决策点单调递增(决策单调性的数学证明较麻烦,本人能力不足,不作探讨)。</p>
<p>维护决策集合用单调队列,查找直线交点用二分,随便搞搞就行了。</p>
<p><strong>时间复杂度为</strong> <span class="math inline">\(O(nlogn)\)</span>。</p>
<h4 id="分治"><strong>【分治】</strong></h4>
<p>为方便描述,用 <span class="math inline">\(dp\)</span> 表示 <span class="math inline">\(dp,dp,dp...dp\)</span>。</p>
<p>假设我们已知 <span class="math inline">\(dp\)</span> 的最优决策点均位于 <span class="math inline">\(\)</span>,再设 <span class="math inline">\(dp\)</span> 的最优决策点为 <span class="math inline">\(j_0\)</span>,其中 <span class="math inline">\(mid=(l+r)/2\)</span>。根据决策单调性的定义可知:<br>
<span class="math inline">\(dp\)</span> 的最优决策点位于 <span class="math inline">\(\)</span>,<br>
<span class="math inline">\(dp\)</span> 的最优决策点位于 <span class="math inline">\(\)</span>。<br>
原问题变成了两个规模更小的同类型问题,所以可以用分治来对 <span class="math inline">\(dp\)</span> 进行优化。</p>
<p>分治的话理解和代码都要简单一些,但在某些情况下可能要被卡,时间复杂度会严重退化,所以还是二分栈的实用性更高。</p>
<p>还是以 <span class="math inline">\(Lightning\)</span> <span class="math inline">\(Conductor\)</span> <span class="math inline">\(\)</span> 为例,每次的分治中先暴力扫一遍找到 <span class="math inline">\(p\)</span> 的最优决策点 <span class="math inline">\(j_0\)</span>,然后做一下左边,再做一下右边,然后 <span class="math inline">\(...\)</span> 然后 <span class="math inline">\(...\)</span> 就没了。</p>
<p><strong>时间复杂度为</strong> <span class="math inline">\(O(nlogn)\)</span>。</p>
<hr>
<h3 id="5code"><strong>5.【Code】</strong></h3>
<h4 id="二分栈-1"><strong>二分栈</strong></h4>
<pre><code class="language-cpp">#include&lt;algorithm&gt;
#include&lt;cstdio&gt;
#include&lt;cmath&gt;
#define Re register int
using namespace std;
const int N=5e5+3;
int i,j,n,h,t,a,Q,Poi;
double p,sqr;
inline void in(Re &amp;x){
    int f=0;x=0;char c=getchar();
    while(c&lt;'0'||c&gt;'9')f|=c=='-',c=getchar();
    while(c&gt;='0'&amp;&amp;c&lt;='9')x=(x&lt;&lt;1)+(x&lt;&lt;3)+(c^48),c=getchar();
    x=f?-x:x;
}
inline double Y(Re i,Re j){return a+sqr;}
inline int find_Poi(int j1,int j2){//找到两个直线的交点i
    Re l=j2,r=n,mid,ans=n+1;//为了处理两个直线没有交点的情况,用一个变量记录答案
    while(l&lt;=r){
      mid=l+r&gt;&gt;1;
      if(Y(mid,j1)&lt;=Y(mid,j2))ans=mid,r=mid-1;
//当前这个位置i使得直线j1的纵坐标小于直线j2的纵坐标,说明这个点i在交点的右方,所以右边界要缩小
      else l=mid+1;
    }
    return ans;
}
inline void sakura(){
    h=1,t=0;
    for(i=1;i&lt;=n;++i){//由于i本身也是一个决策点,所以要先入队再取答案择优
            while(h&lt;t&amp;&amp;Poi&gt;=find_Poi(Q,i))--t;//如果出现了上述可踢的情况,出队
            Poi=find_Poi(Q,i),Q[++t]=i;
            while(h&lt;t&amp;&amp;Poi&lt;=i)++h;
//找到第一个位置j使得直线Q与直线Q的交点大于i,
//那么直线Q就是i前面在最上面的直线,即答案,自己画个图模拟一下就懂了
            p=max(p,Y(i,Q));
    }
}
int main(){
    in(n);
    for(Re i=1;i&lt;=n;++i)in(a),sqr=sqrt(i);
    sakura();
    for(Re i=1;i&lt;n-i+1;++i)swap(a,a),swap(p,p);
    sakura();
    for(Re i=n;i;--i)printf("%d\n",(int)ceil(p)-a);
}
</code></pre>
<h4 id="分治-1"><strong>分治</strong></h4>
<pre><code class="language-cpp">#include&lt;algorithm&gt;
#include&lt;cstdio&gt;
#include&lt;cmath&gt;
#define Re register int
using namespace std;
const int N=5e5+3;
int i,j,n,h,t,a,Q,Poi;
double tmp,p,sqr;
inline void in(Re &amp;x){
    int f=0;x=0;char c=getchar();
    while(c&lt;'0'||c&gt;'9')f|=c=='-',c=getchar();
    while(c&gt;='0'&amp;&amp;c&lt;='9')x=(x&lt;&lt;1)+(x&lt;&lt;3)+(c^48),c=getchar();
    x=f?-x:x;
}
inline void sakura(Re l,Re r,Re L,Re R){
    if(l&gt;r)return;
    Re mid=l+r&gt;&gt;1,j0;double mx=0;
    for(Re j=L;j&lt;=mid&amp;&amp;j&lt;=R;++j)
    //暴力找p的最优决策点j0,而其决策点j必须满足j&lt;=i,即此处的j&lt;=mid
            if((tmp=a+sqr)&gt;mx)mx=tmp,j0=j;
    p=max(p,mx);
    sakura(l,mid-1,L,j0),sakura(mid+1,r,j0,R);
}
int main(){
    in(n);
    for(Re i=1;i&lt;=n;++i)in(a),sqr=sqrt(i);
    sakura(1,n,1,n);
    for(Re i=1;i&lt;n-i+1;++i)swap(a,a),swap(p,p);
    sakura(1,n,1,n);
    for(Re i=n;i;--i)printf("%d\n",(int)ceil(p)-a);
}
</code></pre>
<hr>
<h3 id="6题目链接"><strong>6.【题目链接】</strong></h3>
<h4 id="中档题"><strong>【中档题】</strong></h4>
<ul>
<li>
<p><span class="math inline">\(Lightning\)</span> <span class="math inline">\(Conductor\)</span> <span class="math inline">\(\)</span><br>
【标签】动态规划/决策单调性</p>
</li>
<li>
<p><span class="math inline">\(Noi\)</span> 嘉年华 <span class="math inline">\(\)</span><br>
【标签】动态规划/决策单调性</p>
</li>
<li>
<p>大佬 <span class="math inline">\(\)</span><br>
【标签】动态规划/决策单调性</p>
</li>
</ul>
<h4 id="高档题-1"><strong>【高档题】</strong></h4>
<ul>
<li>诗人小 <span class="math inline">\(G\)</span> <span class="math inline">\(\)</span><br>
【标签】动态规划/单调队列/贪心</li>
</ul>
<hr>
<p></p><div class="math display">\[QAQ
\]</div><p></p><hr>
<h2 id="四单调队列优化-dp"><strong>四:【单调队列优化 DP】</strong></h2>
<hr>
<h3 id="前言-2"><strong>【前言】</strong></h3>
<p>形如 <span class="math inline">\(dp=max/min \{ dp+funtion(i)+function(j) \}\)</span> 的 <span class="math inline">\(dp\)</span> 方程均可尝试使用单调队列优化。</p>
<p><strong>单调栈</strong>和<strong>单调队列</strong>给我们展现出了一种思想:在保证正确性的前提下,及时排除不可能的决策点,保持决策集合内部的有序性和查找决策的高效性。之前的<strong>二分栈</strong>,此处的<strong>单调队列优化</strong>,和后面的<strong>斜率优化</strong>都是以此为核心来运作的。</p>
<p>其实质是优化 <strong>“决策”</strong>。</p>
<hr>
<h3 id="1单调队列的简单运用"><strong>1.【单调队列的简单运用】</strong></h3>
<h4 id="t1"><strong>【T1】</strong></h4>
<p>琪露诺 <span class="math inline">\(\)</span>(盗版滑动窗口QAQ)。</p>
<h5 id="题目大意"><strong>【题目大意】</strong></h5>
<p>在给定序列中找出一条路径使其经过的点之和最大,且每次可走的距离在给定区间 <span class="math inline">\(\)</span> 以内。</p>
<h5 id="分析"><strong>【分析】</strong></h5>
<p>方程非常简单:<span class="math inline">\(dp=max\{ dp+a \} (i-r \leqslant j \leqslant i-l)\)</span> 。</p>
<p>在某一次决策中,由于决策点 <span class="math inline">\(j\)</span> 只可能在 <span class="math inline">\(\)</span> 这一段区间内,可以只将这些点放入决策集合。<br>
而 <span class="math inline">\(l,r\)</span> 是定值,当 <span class="math inline">\(i\)</span> 不断增大时,之前小于 <span class="math inline">\(i-l\)</span> 的 <span class="math inline">\(j\)</span> 现在还是小于 <span class="math inline">\(i-l\)</span>,所以可以永远地踢掉。<br>
若 <span class="math inline">\(j &lt; j'\)</span> 且 <span class="math inline">\(dp \leqslant dp\)</span>,那么 <span class="math inline">\(dp\)</span> 也可以永远地踢掉。为什么呢? <span class="math inline">\(j\)</span> 在 <span class="math inline">\(j'\)</span> 的前面,一定会先成为不合法决策点,而 <span class="math inline">\(j\)</span> 的价值又比 <span class="math inline">\(j'\)</span> 小,因此留下来只是浪费扫描的时间。</p>
<p>最终维护出了一个价值递减的单调队列。</p>
<h5 id="code"><strong>【Code】</strong></h5>
<pre><code class="language-cpp">#include&lt;algorithm&gt;
#include&lt;cstring&gt;
#include&lt;cstdio&gt;
#define Re register int
using std::max;
const int N=2e5+3;
int n,l,r,h=1,t,a,Q;
long long ans=-2e9,dp;
inline void in(Re &amp;x){
    int f=0;x=0;char c=getchar();
    while(c&lt;'0'||c&gt;'9')f|=c=='-',c=getchar();
    while(c&gt;='0'&amp;&amp;c&lt;='9')x=(x&lt;&lt;1)+(x&lt;&lt;3)+(c^48),c=getchar();
    x=f?-x:x;
}
int main(){
    in(n),in(l),in(r);
    memset(dp,-127,sizeof(dp));
    Q[++t]=0;
    for(Re i=0;i&lt;=n;++i)in(a);
    dp=a;
    for(Re i=l;i&lt;=n;i++){//注意枚举起点是l不是1
            while(h&lt;=t&amp;&amp;dp]&lt;=dp)--t;//维护单调递减
            Q[++t]=i-l;//入队
            while(h&lt;=t&amp;&amp;Q&lt;i-r)++h;//保证决策点合法
            dp=dp]+a;//取出最优决策点
            if(i&gt;n-r)ans=max(ans,dp);
    }
    printf("%lld",ans);
}
</code></pre>
<h4 id="t2"><strong>【T2】</strong></h4>
<p><span class="math inline">\(fence\)</span> <span class="math inline">\(\)</span></p>
<h5 id="题目大意-1"><strong>【题目大意】</strong></h5>
<p><span class="math inline">\(M\)</span> 个工人要对 <span class="math inline">\(N\)</span> 块木板进行粉刷。工人 <span class="math inline">\(i\)</span> 要么不刷,要么就刷不超过 <span class="math inline">\(L_i\)</span> 块并且包含 <span class="math inline">\(S_i\)</span> 的连续一段木板,每粉刷一块木板有 <span class="math inline">\(P_i\)</span> 的报酬。要求使总报酬最大。</p>
<h5 id="分析-1"><strong>【分析】</strong></h5>
<p><span class="math inline">\(S_i\)</span> 的散乱分布非常讨厌,所以先把工人按 <span class="math inline">\(S_i\)</span> 排个序。</p>
<p>主要信息有“工人序号”,“木板序号”,“报酬”这三个,而其中“报酬”为所求答案,可以用 <span class="math inline">\(dp\)</span> 表示前 <span class="math inline">\(i\)</span> 个工人刷完前 <span class="math inline">\(j\)</span> 块木板所得总报酬。</p>
<p>考虑状态转移:<br>
第 <span class="math inline">\(i\)</span> 个工人可以跳过不刷木板,也可以跳过第 <span class="math inline">\(j\)</span> 块木板不刷,因此先在 <span class="math inline">\(dp\)</span> 和 <span class="math inline">\(dp\)</span> 当中取个最大值。<br>
工人 <span class="math inline">\(i\)</span> 想要粉刷的区间 <span class="math inline">\(\)</span> 必须包括 <span class="math inline">\(S_i\)</span>,并且区间长度要小于等于 <span class="math inline">\(L_i\)</span>。<br>
所以得出 <span class="math inline">\(dp\)</span> 转移方程:<span class="math inline">\(dp=max \{ dp,dp,dp+P_i*(j-k) \}\)</span>,其中 <span class="math inline">\(S_i \leqslant j\)</span> 且 <span class="math inline">\(k \in \)</span>。</p>
<p><span class="math inline">\(k\)</span> 为决策点,<span class="math inline">\(P_i*j\)</span> 为定值可以单独提出来,所以实际上就是上面琪露诺 <span class="math inline">\(\)</span>一样的类型,只是加了一维而已。</p>
<p>最终维护出了一个 <span class="math inline">\(dp-P_i*k\)</span> 递减的单调队列。</p>
<h5 id="code-1"><strong>【Code】</strong></h5>
<pre><code class="language-cpp">#include&lt;algorithm&gt;
#include&lt;cstring&gt;
#include&lt;cstdio&gt;
#define Re register int
using namespace std;
const int N=16005;
struct QAQ{int L,P,S;}a;
int n,m,i,j,k,Q,W,dp;
inline bool cmp(QAQ a,QAQ b){return a.S&lt;b.S;}
int main(){
    while(scanf("%d%d",&amp;n,&amp;m)!=EOF){
            memset(dp,0,sizeof(dp));
            memset(a,0,sizeof(a));
            for(i=1;i&lt;=m;++i)scanf("%d%d%d",&amp;a.L,&amp;a.P,&amp;a.S);
            std::sort(a+1,a+m+1,cmp);
            for(i=1;i&lt;=m;++i){
            Re l=1,r=0,tmp,Si=a.S,Li=a.L,Pi=a.P;
            for(k=max(0,Si-Li);k&lt;Si;++k){
            //k+1为工人i开始刷的位置,max(1,Si-Li+1)&lt;=k+1&lt;=Si
                    tmp=dp-Pi*k;
                    while(l&lt;=r&amp;&amp;Q&lt;=tmp)--r;
                    Q[++r]=tmp,W=k;
            }
            for(j=1;j&lt;=n;++j){
                    dp=max(dp,dp);
                if(Si+Li&gt;j&amp;&amp;j&gt;=Si){//Si+Li-1&gt;=j&gt;=Si
                  while(l&lt;=r&amp;&amp;W+Li&lt;j)++l;//W+1+Li-1&lt;j
                  if(l&lt;=r)dp=max(dp,Q+Pi*j);
                }
            }
            }
            printf("%d\n",dp);
    }
}
</code></pre>
<hr>
<h3 id="2单调队列优化多重背包"><strong>2.【单调队列优化多重背包】</strong></h3>
<p>先来回顾一下多重背包问题。</p>
<p>用 <span class="math inline">\(v,w,c\)</span> 分别表示物品重量,价值,数量,<span class="math inline">\(n\)</span> 为物品数量,<span class="math inline">\(V\)</span> 为背包容量。<span class="math inline">\(dp\)</span> 方程为:<span class="math inline">\(dp=max\{ dp]+k*w \}\)</span> <span class="math inline">\((j \in ,V],\)</span> <span class="math inline">\(k \in ,j/v)])\)</span></p>
<p>还可以用二进制拆分来进行优化,但就是有这样一道题,连 <span class="math inline">\(log\)</span> 都要卡:多重背包 <span class="math inline">\(\)</span>。所以还需要考虑更高效的算法。</p>
<p>但说来说去似乎都和单调队列扯不上关系。</p>
<p>为何?</p>
<p>观察 <span class="math inline">\(dp\)</span> 和 <span class="math inline">\(dp\)</span> 决策集合:<br>
<span class="math inline">\(dp: \{ j \% v...j-2*v,j-v \}\)</span><br>
<span class="math inline">\(dp: \{ (j-1) \% v...(j-1)-2*v,(j-1)-v \}\)</span></p>
<p><span class="math inline">\(dp\)</span> 的每一个决策点都与 <span class="math inline">\(dp\)</span> 不同,很难根据 <span class="math inline">\(dp\)</span> 的决策集合转移成 <span class="math inline">\(dp\)</span> 的决策集合。</p>
<p>再看 <span class="math inline">\(dp\)</span> 和 <span class="math inline">\(dp]\)</span>:</p>
<p><span class="math inline">\(dp: \{ j-c*v...j-2v,j-v \}\)</span></p>
<p><span class="math inline">\(dp]: \{ j-(c+1)*v...j-3v,j-2v \}\)</span></p>
<p>可以发现上面只是比下面的区别仅仅在于 <span class="math inline">\(j-(c+1)*v\)</span> 和 <span class="math inline">\(j-v\)</span> ,恰好满足单调队列的一个特性:但有新的决策出现时,决策点集合中会去掉一部分不合法的,再加上一部分新来的。</p>
<p>所以我们可以按照 <span class="math inline">\(j%v\)</span> 的余数来分一下:</p>
<p><span class="math inline">\(dp(j\%v=0):\{0,v,2v...j-2v,j-v\}\)</span><br>
<span class="math inline">\(dp(j\%v=1):\{1,v+1,2v+1...j-2v,j-v \}\)</span><br>
<span class="math inline">\(...\)</span><br>
<span class="math inline">\(dp(j\%v=v-1):\{v-1,2v-1...j-2v,j-v\}\)</span></p>
<p>设 <span class="math inline">\(j=p*v+r\)</span>,那么原方程可改为: <span class="math inline">\(dp+r]=max\{ dp]+(p-k)*w \}\)</span> <span class="math inline">\((r \in -1],\)</span> <span class="math inline">\(p \in \rfloor],\)</span> <span class="math inline">\(k \in \rfloor ,c),p])\)</span><br>
只要在最外层将 <span class="math inline">\(i,r,p\)</span> 都枚举出来,这就是一个标准的单调队列可优化方程,用类似 <span class="math inline">\(fence\)</span> <span class="math inline">\(\)</span> 的方法维护即可。</p>
<p>时间复杂度为 <span class="math inline">\(O(N*V)\)</span> 。</p>
<h4 id="code-2"><strong>【Code】</strong></h4>
<pre><code class="language-cpp">#include&lt;cstdio&gt;
#define Re register int
const int N=7003,M=7003;
int n,h,t,V,mp,tmp,v,w,c,Q,K,dp;
inline void in(Re &amp;x){
    Re fu=0;x=0;char ch=getchar();
    while(ch&lt;'0'||ch&gt;'9')fu|=ch=='-',ch=getchar();
    while(ch&gt;='0'&amp;&amp;ch&lt;='9')x=(x&lt;&lt;1)+(x&lt;&lt;3)+(ch^48),ch=getchar();
    x=fu?-x:x;
}
inline int max(Re a,Re b){return a&gt;b?a:b;}
inline int min(Re a,Re b){return a&lt;b?a:b;}
int main(){
    in(n),in(V);
    for(Re i=1;i&lt;=n;++i)in(v),in(w),in(c);
    for(Re i=1;i&lt;=n;++i)
            for(Re r=0;r&lt;v;++r){
            h=1,t=0,mp=(V-r)/v;
            for(Re p=0;p&lt;=mp;++p){
                    tmp=dp+r]-w*p;
                    while(h&lt;=t&amp;&amp;Q&lt;=tmp)--t;
                    Q[++t]=tmp,K=p;
                    while(h&lt;=t&amp;&amp;p-K&gt;min(c,V/v))++h;
                    dp+r]=max(dp+r],Q+p*w);
            }
      }
    printf("%d",dp);
}
</code></pre>
<hr>
<h3 id="3题目链接-1"><strong>3.【题目链接】</strong></h3>
<h4 id="简单题-1"><strong>【简单题】</strong></h4>
<ul>
<li>琪露诺 <span class="math inline">\(\)</span><br>
【标签】动态规划/单调队列</li>
</ul>
<h4 id="中档题-1"><strong>【中档题】</strong></h4>
<ul>
<li>
<p><span class="math inline">\(fence\)</span> <span class="math inline">\(\)</span><br>
【标签】动态规划/单调队列</p>
</li>
<li>
<p>多重背包 <span class="math inline">\(\)</span><br>
【标签】动态规划/单调队列/多重背包</p>
</li>
<li>
<p>我要长高 <span class="math inline">\(\)</span><br>
【标签】动态规划/单调队列</p>
</li>
</ul>
<hr>
<p></p><div class="math display">\[QAQ
\]</div><p></p><hr>
<h2 id="五斜率优化-dp"><strong>五:【斜率优化 DP】</strong></h2>
<p>由于篇幅过大,已搬出。。。。。</p>
<p>【学习笔记】动态规划—斜率优化DP(超详细)</p>
<p>补充:其实质是优化 <strong>“决策”</strong>。</p>
<hr>
<p></p><div class="math display">\[QAQ
\]</div><p></p><hr>
<h2 id="参考文献"><strong>【参考文献】</strong></h2>
<p>(本文部分内容摘自以下文章)</p>
<ul>
<li>
<p><span class="math inline">\(dp\)</span> 的各种优化</p>
</li>
<li>
<p>决策单调性优化</p>
</li>
<li>
<p>四边形不等式优化</p>
</li>
<li>
<p>四边形不等式学习笔记</p>
</li>
<li>
<p>四边形不等式优化讲解(详解)</p>
</li>
<li>
<p>单调队列优化的多重背包</p>
</li>
<li>
<p>动态规划的单调队列优化(含多重背包)</p>
</li>
<li>
<p><span class="math inline">\(dp\)</span> 基础 — <span class="math inline">\(DraZxINDdt\)</span></p>
</li>
<li>
<p>《算法竞赛进阶指南》李煜东</p>
</li>
<li>
<p>《动态规划算法的优化技巧》毛子青</p>
</li>
</ul>


</div>
<div id="MySignature" role="contentinfo">
    <div>
<img alt="知识共享许可协议" style="border-width:0" src="https://images.cnblogs.com/cnblogs_com/Xing-Ling/1635185/o_20062410573688x31.png" />

本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
</div>

<br />

<div>作者:辰星凌 (Xing_Ling)</div>

<p>-----若要转载请私信作者获得许可并在文首标出转载来源-----</p><br><br>
来源:https://www.cnblogs.com/Xing-Ling/p/11317315.html
頁: [1]
查看完整版本: 【学习笔记】动态规划—各种 DP 优化