前言
这几天在学习点分治,写了一百多行代码交模板题直接爆零,所以转而研究DP,发现了一些好题以及坑点。
由于太坑,我今天决定转而继续调试点分治。
1:4933 大师
我的思路参考题解区O(n2)做法,AC代码如下:
#include<iostream>
using namespace std;
long long a[1005];
long long f[1005][40005];
const int N=20005;
long long sum=0;
int main() {
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
(f[i][a[i]-a[j]+N]+=1+f[j][a[i]-a[j]+N])%=998244353,
(sum+=1+f[j][a[i]-a[j]+N])%=998244353;
cout<<sum+n;
}
但是在具体实现时,我发现一个问题。如果将状态的转移和统计sum分离,这样是正确的:
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
(f[i][a[i]-a[j]+N]+=1+f[j][a[i]-a[j]+N])%=998244353;
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
(sum+=1+f[j][a[i]-a[j]+N])%=998244353;
而如果这样统计sum,那就会获得偏大的答案:
for(int j=1;j<i;j++)
(f[i][a[i]-a[j]+N]+=1+f[j][a[i]-a[j]+N])%=998244353;
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
(sum+=f[i][a[i]-a[j]+N])%=998244353;
经过与机房同学的讨论,得到了如下代码,可以通过该题:
for(int i=1;i<=n;i++)
for(int j=1;j<40005;j++)
(sum+=f[i][j])%=998244353;
除这3行代码以外,其余部分均与文中第一次提到的AC代码相同。
与上文中无法通过的代码相比,这一份代码通过枚举公差来更新sum。
于是可以明显的看出,上一份代码的问题在于可能有a[i]-a[j1]=a[i]-a[j2]的情况,所以导致一些状态被计算了多次。因此答案会偏大。
2:P3558 [POI2013]BAJ-Bytecomputer
我的思路基本与题解一致,但在转移的过程中,与绝大部分题解通过手动枚举所有情况进行转移不同的是,我通过推导公式帮助转移。
状态、初始化与转移
设f[i][j+1]表示维持前i个数字为升序,并且第i个数字变为j的最小操作次数。显然j∈[-1,1],也就是说j不可能超出原有边界,设转移时,第i-1个数字变为了k,那么证明如下:
先证明j≥-1:这是因为k≥-1,且j≥k,用数学归纳法可以证明。
再证明j≤1:如果j>1,那么j后面的每一个数都必须>1,也就是说,j以及后面的数都"向上偏移"了,这时候完全可以减少j的增量,让j以及其后面的数再“向下偏移”,通过减小j累加k的次数来实现“向下偏移”,显然这样可以减少操作次数。
初始化:f[a[1]+1]=0,显然第一个数直接构成一个不下降序列,而且第一个数无法改变。其他所有地方赋初值为1e9,表示(在当前以及之前条件下)无论进行多少次操作也无法得到这些局面。比如在i=1条件下,无论进行多少次操作也无法改变f[2]区域的值。
结尾特判,如果答案≥1e9,意味着无解,输出BRAK。
转移:
- 首先讨论是否a[i]是否需要通过累加k来达到j,(即a[i]是否等于j)。如果不需要,直接继承前一个位置的值即可:
if(a[i]==j)
f[i][j+1]=min(f[i][j+1],f[i-1][k+1]);
- 否则,即ai≠j,那么设累加j的次数为cnt,于是得到如下有关k的方程:
k×cnt+ai=j
解方程:
cnt=(j-ai)/k ,注意这里的"/"表示整除,换言之,若无法整除则无解。
其他无解的情况有:k=0;(j-ai)/k<0,这种情况无解是因为显然操作次数不可能为负数。
接下来讨论这3种无解的情况:
- 无法整除时,不存在合法的cnt,跳过这种状态(continue)。
- cnt<0,不存在合法的cnt,continue。
- k=0,不存在合法的cnt。(严格来说这里忽略了一种情况,即ai=j且k=0,根本不需要改变,此时cnt=0,但前文已经提到了ai≠j,所以没有讨论这种情况也是可以的)
即:
else if(!k||(c=((j-a[i])/k))<0||(j-a[i])%k)
continue;
- 否则,用c代表cnt,可以得到以下转移方程:
else
f[i][j+1]=min(f[i][j+1],f[i-1][k+1]+c);
这个公式的语义是,这个位置为j的最小操作次数=所有“前一个位置为k的最小操作次数+k变成j最少需要的操作次数”中的最小值。
最后,注意到序列必须是不降的,所以任何时候,k<=j。
f[1]由上文的初始化得到,是故转移从f[2]开始:
for(int i=2; i<=n; i++)
for(int j=-1; j<=1; j++)
for(int k=-1,c=0; k<=j; k++)
if(a[i]==j)
f[i][j+1]=min(f[i][j+1],f[i-1][k+1]);
else if(!k||(c=((j-a[i])/k))<0||(j-a[i])%k)
continue;
else
f[i][j+1]=min(f[i][j+1],f[i-1][k+1]+c);
完整代码:
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
int a[1000005];
int f[1000005][3];
int main() {
int n;
cin>>n;
for(auto&i:f)
for(auto&j:i)
j=1e9;
for(int i=1; i<=n; i++)
cin>>a[i];
f[1][a[1]+1]=0;
for(int i=2; i<=n; i++)
for(int j=-1; j<=1; j++)
for(int k=-1,c=0; k<=j; k++)
if(a[i]==j)
f[i][j+1]=min(f[i][j+1],f[i-1][k+1]);
else if(!k||(c=((j-a[i])/k))<0||(j-a[i])%k)
continue;
else
f[i][j+1]=min(f[i][j+1],f[i-1][k+1]+c);
if(min({f[n][0],f[n][1],f[n][2]})>=1e9)
cout<<"BRAK";
else
cout<<min({f[n][0],f[n][1],f[n][2]});
return 0;
}
本题后记
这个公式确实很不容易推导,而且极其容易漏解,感谢机房同学甲和机房同学乙的调试!
后记
于是皆大欢喜。
本文介绍了在解决编程题目时,动态规划(DP)和分治策略的应用。作者通过两个具体的例子,4933大师问题和P3558[POI2013]BAJ-Bytecomputer问题,详细阐述了这两种算法的实现细节和易错点。在4933大师问题中,作者发现状态转移和统计sum的分离对于避免重复计算和确保正确答案至关重要。而在P3558问题中,通过数学推导得出状态转移公式,避免了手动枚举所有情况,提高了效率。文章强调了正确理解和应用算法的重要性,并感谢同行的帮助。
1万+

被折叠的 条评论
为什么被折叠?



