[APIO2016] 划艇(dp + 组合数 + 前缀和优化)

本文详细解析了洛谷P3643题目的解决方案,采用动态规划方法,讨论了如何处理区间计数并保证序列递增的约束。通过离散化技巧将问题简化,最后实现的时间复杂度为O(n^3)。文中还给出了核心的代码实现,帮助理解算法思路。
摘要由CSDN通过智能技术生成

problem

luogu-P3643

solution

有个显然的暴力 d p dp dp。设 d p ( i , j ) : dp(i,j): dp(i,j): 到了第 i i i 个学校,其参加且派出 j j j 个划艇的方案数。

枚举上一个参加的学校以及派出的划艇,则有转移: d p ( i , j ) = ∑ k < i , j < j d p ( k , j ′ ) dp(i,j)=\sum_{k<i,j<j} dp(k,j') dp(i,j)=k<i,j<jdp(k,j)

可以再套个前缀和优化,但是由于第二维可以达到 1 e 9 1e9 1e9,并没有起到关键性优化。

实际上我们并不关系真的派出了多少个划艇,我们只在乎之间的满足的递增关系。

所以我们可以考虑离散化成 O ( 2 n ) O(2n) O(2n) 个端点。 [ a i , a i + 1 ) → i [a_i,a_{i+1})\rightarrow i [ai,ai+1)i

f ( i , j ) : f(i,j): f(i,j): i i i 所学校中,第 i i i 所学校参赛,且派出的划艇数属于第 j j j 个区间内的方案数。

  • Lemma : \text{Lemma}: Lemma: 从区间 [ 0 , L ] [0,L] [0,L] 中取 n n n 个数,要求所有非零数严格递增,方案数为 ( L + n n ) \binom {L+n}n (nL+n)

Proof : \text{Proof}: Proof:

  • 没有 0 0 0 的情况,答案肯定是 ( L n ) \binom Ln (nL)。因为如果确定了一种组合,那么方案也随之确定,即为这个组合的从小到大排列。所以这二者存在一一对应的关系。
  • 0 0 0 。观察这个序列 0 0 0 ... 0 1 2 ... L \text{0 0 0 ... 0 1 2 ... L} 0 0 0 ... 0 1 2 ... L。考虑从中选 n n n 个数,取某个非零数 i i i 对应没取 0 0 0 的第 i i i 次选 i i i

现在由于第 i i i 所学校必须参赛,所以计算的时候 0 0 0 的个数 − 1 -1 1。方案数即 ( L + m − 1 m ) \binom{L+m-1}m (mL+m1)

其中 m m m 表示选划艇个数包含第 j j j 个区间的学校数量。

对于一个 k k k,对应方案数为 ( L + m − 1 m ) ∑ j ′ < j f ( k , j ′ ) \binom {L+m-1}m\sum_{j'<j}f(k,j') (mL+m1)j<jf(k,j)

所以 f ( i , j ) = ∑ k < i ( L + m − 1 m ) ∑ j ′ < j f ( k , j ‘ ) f(i,j)=\sum_{k<i}\binom{L+m-1}{m}\sum_{j'<j}f(k,j‘) f(i,j)=k<i(mL+m1)j<jf(k,j)

此时再加前缀和优化, s u m ( k , j ) = ∑ j ′ < j f ( k , j ′ ) sum(k,j)=\sum_{j'<j}f(k,j') sum(k,j)=j<jf(k,j)

f ( i , j ) = ∑ k < i ( L + m k − 1 m k ) s u m ( k , j ) f(i,j)=\sum_{k<i}\binom{L+m_k-1}{m_k}sum(k,j) f(i,j)=k<i(mkL+mk1)sum(k,j)

时间复杂度 O ( n 3 ) O(n^3) O(n3)

code

#include <bits/stdc++.h>
using namespace std;
#define maxn 1005
#define int long long
#define mod 1000000007
int n;
int a[maxn], b[maxn], c[maxn], x[maxn], inv[maxn], sum[maxn];

signed main() {
	scanf( "%lld", &n );
	for( int i = 1;i <= n;i ++ ) {
		scanf( "%lld %lld", &a[i], &b[i] );
		x[i] = a[i], x[i + n] = b[i] + 1;
	}
	sort( x + 1, x + (n << 1 | 1) );
	int m = unique( x + 1, x + (n << 1 | 1) ) - x - 1;
	for( int i = 1;i <= n;i ++ ) {
		a[i] = lower_bound( x + 1, x + m + 1, a[i] ) - x;
		b[i] = lower_bound( x + 1, x + m + 1, b[i] + 1 ) - x;
	}
	sum[0] = c[0] = inv[1] = 1;
	for( int i = 2;i <= n;i ++ ) inv[i] = (mod - mod / i) * inv[mod % i] % mod;
	for( int j = 1;j < m;j ++ ) {
		int len = x[j + 1] - x[j];
		for( int i = 1;i <= n;i ++ ) c[i] = c[i - 1] * (i + len - 1) % mod * inv[i] % mod;//组合数下标不变 所以可以每一次j区间变化时再求
		for( int i = n;i;i -- ) {//由于i与i-1及前面的挂钩所以不能从前往后更新
			if( a[i] <= j and j + 1 <= b[i] ) {
				int o = 1, fi = 0;
//o为满足条件的个数 由于是从后往前的枚举所以o单调递增 每碰到一个合法的k o就要+1
				for( int k = i - 1;~ k;k -- ) {
					fi = (fi + sum[k] * c[o]) % mod;
					if( a[k] <= j and j + 1 <= b[k] ) o ++;
				}
				sum[i] = (sum[i] + fi) % mod;
			}
		}
	}
	int ans = 0;
	for( int i = 1;i <= n;i ++ ) (ans += sum[i]) %= mod;
	printf( "%lld\n", ans );
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值