数学已不易,信息坑更深——DP解决[COCI2006#4] ZBRKA

博客讲述了动态规划在解决COCI2006#4问题中的应用,即计算特定逆序对数量的数列。作者分析了动态规划的难点,提出了从数学角度理解问题的重要性,并分享了TLE代码及改进后的AC代码,强调了在处理大规模数据时取模操作的坑点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码倒是不难,就是有点费博主。
——题记

众所周知,动态规划往往是信竞人学习新课路上的最后一课。对于我个人而言,动态规划确实是不简单的一关。DP,难就难在它的灵活性,要求我们具体问题具体分析。

对,就是语文老师口中的那种具体问题具体分析。
在这里插入图片描述
DP种类中,相对简单的就是背包DP,在这之后就是最长的不下降子序列和不上升子序列。他们简单,主要是因为其中有基本思路甚至是模板。
目前来看,最有难度的是现在博主正在研究的线性DP(虽然子序列问题属于线性DP,但是除了O(n2)的时间复杂度和双重循环我实在没发现什么共同点)这样的动态规划中,寻找DP数组中的各项参量的合理位置是一个难点。并且动态规划,我们还要想明白从什么样的基本状态开始,怎样方便输出。所以一般来说,输出的答案是存储在数组中的数据。
好了,上题

题目

考虑一个由N个整数构成的数列,其中1到N都在数列中出现了恰好一次。
•在这个数列中从左到右任取两个数,如果前者比后者大,那么这对数就是一个逆序对。而整个数列的逆序数就是其中所有逆序对的总数。
例如,数列(1,4,3,2)的逆序数为3,因为存在三个逆序对:(4,3),(4,2)和(3,2)。
•写一个程序,计算有多少长度为N的这种数列,使它的逆序数恰为C。

输入

Input contains 2 natrual numbers:N (N<=1000)ans C(<=10000).

输出

•计算出所求的答案,将它模1 000 000 007后输出

示例输入、输出

输入1
10 1
输出1
9
输入2
4 3
输出2
6
输入3
9 13
输出3
17957

思路

对于这种题来说,我们上来要思考的并不是怎样DP的问题,而是这里面的数学关系。经过博主一个上午的思考,得出以下结论:
对于一个有n个数字组成,逆序对数量为c的1-n组成数列来说,我们一般会采用一种复杂度为O(n2)的计数方法来数它的逆序对数量。以某一个元素为标杆,和它后面的全部数据进行比较,找出逆序对。我的灵感就来源于此。
现在,请大家思考一个问题:
如果我们再多加上一个元素“n+1”并且想要保持这个逆序对数量不变的话,我们有几种方案?
从这个新元素n+1的位置上来考虑的话,有n种大情况,而在这样的n种大情况之下,如果我们要保证逆序对的数量为c的话,我们依然可以构建很多个符合条件的数组。根据题目要求,我们可以保证这个新元素是数列中最大的元素,那么,设该元素在第a个位置上的话,以它为较大值的逆序对数量,也就是由于这个数的加入而额外产生的逆序对的数量就是n-a个。
然后呢?
我们需要求出n个元素中拥有c-(n-a)个逆序对的数列的数量。这就是DP中的一个很重要的数据关系。我们将新元素的位置分别放在n+1个位置上,多构成n-a(a还是最大数的位置)个逆数对。然后利用求出的c-(n-a)个逆序对的数列的数量,∑,就求出了一个新答案。
如此一来就能够算出答案啦!
于是,我心血来潮写出了这个代码👇

TLE代码

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int dp[1001][10001];
int n , c;
int main(){
	scanf("%d%d",&n,&c);
	memset(dp,0,sizeof dp);
	for(int a = 1;a<1001;a++){
		dp[a][0]=1;
	}
	for(int a = 1;a<=c;a++){//c
		for(int b = 1;b<=n;b++){//n
			for(int x = a;x>max(a-b,-1);x--){
				dp[b][a]+=dp[b-1][x];
				dp[b][a]=dp[b][a]%1000000007;
			}
		}
	}
	cout <<dp[n][c];
	return 0; 
	return 0;
} 

上述代码能够得到56分,按照这种算法,在刨除一些弱智数据,能够冲一冲64分及格线。
这当然不是我想要的。
该代码最大的弱点是它的三重循环。在最严重的情况下可以导致我们的时间复杂度平均增加5000倍!

改进

难道,就真的没有一个好的数学公式了吗?
在这里插入图片描述
借助图形化的思维,我们发现,在这样的N-C坐标系中,三角的数据其实就是箭头覆盖两个格子中数据的和。三角形位置的N坐标越长,箭头所需覆盖的距离也就越长。根据我的运算顺序,三角形上方的数据应该已经算好了。
将三角形上方的数据减去个尾,然后再把三角形左侧一加,不就完事了么?
但是实践证明这种方法针对靠近0界的少量数据并不管用,于是乎,我针对数组中的0有着另外一套判定方案。具体可以参见我后面的源代码。

最大的坑

无需多言,这么大的数肯定要随时取模。但是坑,就出在取模的时候。由于各个数据在取模过程中减去的100 000 000 7数量不等,因此在后期数组里就会出现一些特别难处理的负数。对于这种负数,一般的处理方法是不断加100 000 000 7,直到数据大于0为止。这个方法不难,但是问题就在于各位有没有想到这样的招式。

AC代码

#include<iostream>
#include<cstring>
using namespace std;
long long int dp[1001][10001];
int N,C;
int main(){
	cin >> N >> C;
	memset(dp,0,sizeof dp);
	for(int c = 2;c<1001;c++){
		dp[c][0]=1;
		dp[c][1]=c-1;
	}
	int t;
	long long int lc;
	for(int c = 2;c<=C;c++){
		for(int n = 1;n<=N;n++){
			if(c>(n*(n-1))/2){
				dp[n][c]=0;
			}
			else{
			if(c-n<0){
				t = 0;
			}
			else{
				t = dp[n-1][c-n];//扣掉的尾 
			}
			lc=dp[n-1][c];
			lc-=t;
			lc+=dp[n][c-1];
			lc = lc%1000000007;
			while(lc<0){
				lc+=1000000007;
			}
			dp[n][c]=lc;
			}
		}
	}
	cout << dp[N][C];
	return 0;
} 

在这里插入图片描述
好啦,本周的题目就到这里啦~如果觉得文章有用,记得点赞哦!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值