题解:P1437 [HNOI2004] 敲砖块
OMG_NOIP
·
·
个人记录
更大更清晰的图片
前置芝士
动态规划的无后效性
正文
原题面
分析:每一块选取的条件是其上方的两个数都要选。
尝试:
每个数从上往下取,每个数是否能取只和它上面两个数取没取有关。
设 dp_{i,j,k} 为第 (i,j) 个位置选了 k 个数字的最大值。
结果:
发现推不出状态转移方程,因为这样做不符合动态规划的无后效性。
具体哪里不符合动态规划的无后效性?
举例:
如下图的一组样例:
白色方框就是题目中的砖块。
蓝色的部分是我们现在选择的。
需要敲掉的数量就是蓝色砖块的数量,获得的价值就是所有蓝色砖块的价值和。
但如果之前已经选了下图的粉色部分
那么需要敲掉的数量就要减去重合的部分(即下图的紫色部分),获得的价值也要剪掉重合的部分。
因为同一块砖不用敲两次,价值也不能贡献两次。
这个例子说明当前选的砖块不只和它上方的两个砖块有关,还和其他的砖块有关,所以不满足动态规划的无后效性。
知道了为什么不能这样做,那要怎么办呢?
再次分析题目发现:这道题最终解选中的砖块可以描述成若干个三角形相互连接或重叠:
举例:
下图最终解选中的砖块就是三个三角形相互连接或重叠组成的。
我们将最终解的选中的方框的每列最下层点用线画出(图中的红线)试试:
我们发现这条红线有两个性质:
-
如果从左向右观察,红线上的点只能从其左上方行或左下方行连过来的。
-
红线上点其所有右上方点一定全部被选中。
举例第二条性质,如图:
如果你选了上图中红色的点,那么蓝色的点一定全部被选中。紫色的点是之前选的。
那么就把原问题转化为沟画出重叠三角形的廓折线(图中的红线),找到一条合法的路径,使得围在轮廓线内的数字代价和最大。
验证:因为每个砖块只和其左上方和左下方的砖块有关,所以这个做法符合动态规划的无后效性。
具体实现
为了方面处理,先把上图居左对齐:
预处理 got_{i,j} 为选第 (i,j) 个砖块需要选的所有砖块的价值和。拿上图举例,如果你选的点是上图中红色的砖块,那么 got_{i,j} 就是 \text{红色砖块的价值+所有蓝色砖块的价值和}。
选第 (i,j) 个点需要敲掉的砖块数量就是 i\text{(行数)}。拿上图举例,如果你选的点是上图中红色的砖块,那就要敲掉红色砖块和所有蓝色砖块(紫色的是之前已经被敲掉了)。
同样为了方便处理,再把轮廓线那张图居左对齐。

发现每个砖块的状态由它上方或左下方转移过来。
设 $dp_{i,j,k}$ 为在第 $(i,j)$ 个点选了 $k$ 个的最大值。
转移方程:
$$
dp_{i,j,k}(pre[i]\le k\le m)= \max\begin{cases}
dp_{i-1,j,k-i}+got_{i,j} & 从上方转移过来的 \\
dp_{i+1,j-1,k} & 从左下方转移过来的 \\
\end{cases}
$$
为什么 $k$ 的下限是 $pre_i$ ,因为如果你选的数在第 $i$ 行,你至少要选择 $pre_i$ 个砖块才能构成一个完整的三角形。
举例:

上图中,如果你选了红色砖块,那么你至少要也要选择所有蓝色砖块,这样才能构成一个完整的三角形。
最后一点:要额外开一排 $0$ 排,用来表示每一列一个都不取的情况。
建议手模一下题目中给的样例再写代码。
## 代码
```cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,num[55][55],got[55][55],dp[55][55][3000],pre[55];
signed main(){
ios::sync_with_stdio(false);
for(ll i=1;i<=50;i++)pre[i]=pre[i-1]+i;
cin>>n>>m;
for(ll i=1;i<=n;i++){
for(ll j=1;j<=n-i+1;j++){
cin>>num[i][j];
got[i][j]=num[i][j]+got[i-1][j+1];
}
}
for(ll j=1;j<=n;j++){
for(ll i=0;i<=n-j+1;i++){
if(i==0){
for(ll k=1;k<=m;k++)dp[i][j][k]=max(dp[i][j-1][k],dp[i+1][j-1][k]);
continue;
}
for(ll k=pre[i];k<=m;k++)
dp[i][j][k]=max(dp[i-1][j][k-i]+got[i][j],dp[i+1][j-1][k]);
}
}
ll ans=0;
for(ll j=1;j<=n;j++)
for(ll k=1;k<=m;k++)
ans=max(ans,dp[1][j][k]);
cout<<ans<<endl;
return 0;
}
```
如果你有不懂的地方,请私信我,如果是在工作日,我会尽量在12小时内解答。
私信格式:文章链接+具体问题。
比起点赞关注收藏,我更希望你们可以把不懂的地方告诉我
正文结束
## 废话
~~妈的~~ 终于写完了。
我是看了[另一篇题解](https://www.luogu.com.cn/article/afyua1oe)(的图片)学会这道题的。
因为它的内容我没有完全看懂,所以我看着他画的图想出了和他不一样的转移方程(大雾)。