2022.9.30日调试笔记

本文介绍了在解决编程题目时,动态规划(DP)和分治策略的应用。作者通过两个具体的例子,4933大师问题和P3558[POI2013]BAJ-Bytecomputer问题,详细阐述了这两种算法的实现细节和易错点。在4933大师问题中,作者发现状态转移和统计sum的分离对于避免重复计算和确保正确答案至关重要。而在P3558问题中,通过数学推导得出状态转移公式,避免了手动枚举所有情况,提高了效率。文章强调了正确理解和应用算法的重要性,并感谢同行的帮助。

前言

这几天在学习点分治,写了一百多行代码交模板题直接爆零,所以转而研究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种无解的情况:

  1. 无法整除时,不存在合法的cnt,跳过这种状态(continue)。
  2. cnt<0,不存在合法的cnt,continue。
  3. 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;
}

本题后记

这个公式确实很不容易推导,而且极其容易漏解,感谢机房同学甲机房同学乙的调试!

后记

于是皆大欢喜。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值