【HNOI2010 DAY1】合唱队

本文介绍了一种基于动态规划的算法,用于计算从多少种初始队形出发,可以通过特定的排队方式达到理想的合唱队队形。该算法通过定义状态数组f[i][j]和g[i][j]来实现高效计算。

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

Description

为了在即将到来的晚会上有更好的演出效果,作为AAA合唱队负责人的小A需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共有N个人,第i个人的身高为Hi毫米(1000<=Hi<=2000),并且已知任何两个人的身高都不同。假定最终排出的队形是N个人站成一排,为了简化问题,小A想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终排出的队形中: 
- 第一个人直接插入空的当前队形中。 
- 对从第二个人开始的每个人, 
- - 如果他比前面那个人高(H较大),那么将他插入当前队形的最右边。 
- - 如果他比前面那个人矮(H较小),那么将他插入当前队形的最左边。 
当N个人全部插入当前队形后便获得最终排出的队形。 
例如,有6个人站成一个初始队形,身高依次为1850,1900,1700,1650,1800和1750,那么小A会按以下步骤获得最终出的队形: 
- 1850 
- 1850,1900 
1700,1850,1900 
1650,1700,1850,1900 
- 1650,1700,1850,1900,,1800 
1750,1650,1700,1850,1900,1800 
因此,最终排出的队形是1750,1650,1700,1850,1900,1800 
小A心中有一个理想队形,他想知道从多少种初始队形出发能通过上述排队的方式获得他心中的理想队形作为最终排出的队形?

Input

第一行是一个正整数N,表示总人数。 
输入文件第二行是用空格隔开的N个正整数,从左到右表示小A心中的理想队形:H1, H2, „, HN。 

输入的数据保证1000≤Hi≤2000且没有相同的H值. 
其中30%的数据满足1≤N≤100,100%的数据满足1≤N≤1000。

Output

仅包含一个数,表示从多少种初始队形出发能通过上述排队的方式获得输入文件中指定的小A心中的理想队形。 
因为满足条件的初始队形数可能很大,所以规定只要输出满足条件的初始队形数mod 19650827的值。

Sample Input

输入样例1:
4
1701 1702 1703 1704
输入样例2:
4
1704 1703 1702 1701

Sample Output

输出样例1:
8

输出样例2:
0

Hint

样例解释: 
8种初始队形分别为 
(1701, 1702, 1703, 1704) 
(1704, 1703, 1702, 1701) 
(1702, 1701, 1703, 1704) 
(1703, 1702, 1701, 1704) 
(1702, 1703, 1701, 1704) 
(1702, 1703, 1704, 1701) 
(1703, 1704, 1702, 1701) 
(1703, 1702, 1704, 1701)


【分析】

我果然还是太弱了 Orz... 拿到这道题后居然想了很久才联想到DP...

分析题目可以知道,初始队形中的连续一段一定对应目标队形中的连续一段。

我们这样定义状态数组:

    f[i][j]表示目标队形中[i,j]这一段最左边(i)是刚放上去的数的方案数

   g[i][j]表示目标队形中[i,j]这一段最右边(j)是刚放上去的数的方案数

那么我们可以得到状态转移方程:

    f[i][j]=f[i+1][j]*(h[i]<h[i+1])+g[i+1][j]*(h[i]<h[j])

    g[i][j]=f[i][j-1]*(h[j]>h[i])+g[i][j-1]*(h[j]>h[j-1])

 再回过头来看这个状态转移方程,如果题目不要求满足条件才可以放在最左边或者最右边,那么就是个单纯的递推:

    f[i][j]=f[i+1][j]+g[i+1][j]       g[i][j]=f[i][j-1]+g[i][j-1]

而题目加上了对高度的限制,所以我们要判断高度来选择是否继承那些方案,这就是决策。

边界:

    f[i][i]=g[i][i]=1    f[i-1][j]=g[i-1][j]=(h[i-1]<h[i])


【代码】

/*
    ID:Ciocio
	LANG:C++
	DATE:2013-12-07
	TASK:Chorus
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

#define MAXN 1005
#define MODER 19650827

int N;
int h[MAXN];
int f[MAXN][MAXN],g[MAXN][MAXN];

void _init()
{
	scanf("%d",&N);
	for(int i=1;i<=N;i++)
		scanf("%d",&h[i]);
}

void _solve()
{
	for(int i=1;i<=N;i++)
	{
		f[i][i]=g[i][i]=1;
		if(i!=1) f[i-1][i]=g[i-1][i]=(h[i-1]<h[i]);
	}
	for(int k=3;k<=N;k++)
		for(int p=1;p+k-1<=N;p++)
		{
			int i=p,j=p+k-1;
			f[i][j]=f[i+1][j]*(h[i]<h[i+1])+g[i+1][j]*(h[i]<h[j]);
			g[i][j]=f[i][j-1]*(h[j]>h[i])+g[i][j-1]*(h[j]>h[j-1]);
			f[i][j]%=MODER;
			g[i][j]%=MODER;
		}
	cout<<(f[1][N]+g[1][N])%MODER<<endl;
}

int main()
{
	_init();
	_solve();
	return 0;
}


<think>好的,我现在需要解决用户关于[HNOI2010]弹飞绵羊的问题,特别是分块方法的解题思路。首先,我需要回忆一下这个题目的具体内容以及分块算法的一般应用。 题目大意应该是每个装置有一个弹力系数,当绵羊到达某个位置时会被弹到后面的某个位置,直到弹飞为止。问题应该是要求动态维护弹飞所需的步数,或者支持修改某个位置的弹力系数,同时查询某个点弹飞所需的次数。 分块处理这种问题通常是将数据分多个块,每个块维护一些预处理的信息,这样在查询和修改时可以降低时间复杂度。比如,对于每个位置i,可以预处理出跳出当前块需要的步数,以及跳出后的落点位置。这样查询时就可以逐块跳跃,减少计算量。 接下来,我需要具体思考分块如何在这里应用。每个块的大小通常取√n,这样总共有√n个块。对于每个位置i,在预处理阶段,需要计算从i出发,需要多少步才能跳出当前所在的块,并且记录跳出后的位置。这样,当查询某个点的弹飞步数时,只需要累加每个块的步数,直到弹飞为止。修改操作时,只需要更新所在块内的信息,因为块内的预处理信息可能被影响,但其他块不受影响。 例如,假设块大小为m,对于每个位置i,如果i + a[i]超出了当前块的范围,则step[i] = 1,next[i] = i + a[i]。否则,step[i] = step[i + a[i]] + 1,next[i] = next[i + a[i]]。这样预处理之后,查询时从当前位置开始,每次跳到next[i],并累加step[i],直到next[i]超过n,即弹飞。 修改操作时,比如修改位置k的弹力系数,那么需要从k所在块的起始位置开始,重新计算该块内所有位置的step和next。这是因为修改可能影响该块内其他位置的预处理结果。比如,如果某个位置j的next[j]原本指向k的位置,修改k的弹力系数会影响j的next和step,所以需要重新计算整个块的信息。 这样,每次查询的时间复杂度是O(√n),因为每个块最多跳一次,而块的数量是√n。修改操作的时间复杂度是O(√n),因为需要重新处理整个块,大小是√n。 需要注意的是,分块的具体实现需要确定块的大小,通常取√n,但有时根据实际情况调整可能会有更好的效果。另外,预处理每个块的step和next时,需要从块的末尾向前处理,因为后面的位置的处理结果可能影响前面的位置。 可能还需要考虑边界条件,比如弹力系数是否可能超过数组长度,或者弹飞的条件。比如,当i + a[i] >= n时,就算弹飞,此时step[i]=1,next[i]=n或某个超出范围的标记。 另外,在实现过程中,如何处理块的分界点,以及如何遍历每个块内的元素,都是需要注意的细节。例如,块的范围是左闭右开还是左闭右闭,需要统一处理,避免数组越界或者漏掉元素。 总结一下,解题步骤应该是: 1. 将整个数组分若干块,块的大小约为√n。 2. 预处理每个块内的每个位置i的step[i]和next[i],处理顺序应该是从块的最后一个元素向前处理。 3. 查询时,从查询点开始,不断跳到next[i],并累加step[i],直到i超出数组范围,总步数即为答案。 4. 修改时,找到修改位置所在的块,重新预处理该块内的所有元素的step和next。 这样,分块方法能够将时间复杂度降到O(m√n),其中m是操作次数,适用于较大的数据规模,符合算法竞赛的要求。 在代码实现时,需要注意块的下标处理,比如块号从0开始还是1开始,块的大小是否需要调整以适应数组长度不能被整除的情况。此外,预处理函数需要正确计算每个块的起点和终点,并在块内逆序处理元素,以确保后面的元素先被处理,前面的元素可以正确引用后面的step和next值。 举个例子,假设数组长度n=10,块大小m=3,那么块0是0-2,块1是3-5,块2是6-8,块3是9。对于每个块内的元素,比如块0,从位置2开始处理到0。对于位置i=2,如果a[2]=4,则i+a[i]=6,超过块0的范围,所以step[2]=1,next[2]=6。然后处理i=1,假设a[1]=2,则i+a[i]=3,属于块1,所以step[1]=1,next[1]=3。接着处理i=0,假设a[0]=1,则i+a[i]=1,仍在块0内,所以step[0] = step[1] + 1 = 2,next[0] = next[1] =3。这样,当查询位置0时,会先跳到3,再跳到next[3]的位置,依此类推,直到弹飞。 因此,分块方法的关键在于预处理每个块内的跳跃信息,使得查询时可以快速跨块计算,而修改时只需要更新所在块的信息,从而将时间复杂度降低到可接受的范围。</think>### [HNOI2010]弹飞绵羊分块解法思路 #### 问题分析 题目要求维护一个包含$n$个弹簧的序列,每个弹簧有弹力系数$k_i$。当绵羊落在位置$i$时,会被弹到$i + k_i$的位置,直到弹飞(即位置$\geq n$)。需要支持两种操作: 1. **查询**:从位置$x$出发,弹多少次会被弹飞? 2. **修改**:将位置$x$的弹力系数改为$y$。 #### 分块思想 将序列分为$\sqrt{n}$个块,每个块大小约为$\sqrt{n}$。预处理每个位置的**跳跃步数**和**落点**,使得: - **块内跳跃信息**:对于每个位置$i$,记录跳出当前块所需的步数$step[i]$和最终落点$next[i]$。 - **查询优化**:每次查询只需逐块跳跃,时间复杂度$O(\sqrt{n})$。 - **修改优化**:修改仅影响当前块的信息,时间复杂度$O(\sqrt{n})$。 #### 预处理方法 1. **块划分**:将数组划分为$m = \lceil \sqrt{n} \rceil$个块,每个块大小为$m$。 2. **逆序处理**:从每个块的最后一个位置向前遍历: - 若$i + k_i$超出当前块,则$step[i] = 1$,$next[i] = i + k_i$。 - 若未超出,则继承下一个位置的步数和落点: $$step[i] = step[i + k_i] + 1, \quad next[i] = next[i + k_i]$$ #### 查询操作 从位置$x$开始,不断跳转到$next[x]$并累加$step[x]$,直到$next[x] \geq n$。总步数即为答案。 #### 修改操作 1. 找到位置$x$所在的块。 2. **重置块内信息**:从该块的最后一个位置重新逆序计算$step$和$next$。 #### 代码框架(Python示例) ```python import math class BlockSolution: def __init__(self, n, k): self.n = n self.k = k.copy() self.block_size = int(math.sqrt(n)) + 1 self.step = [0] * n self.next = [0] * n self.build_blocks() def build_blocks(self): # 分块预处理 for block_start in range(0, self.n, self.block_size): block_end = min(block_start + self.block_size, self.n) for i in range(block_end - 1, block_start - 1, -1): if i + self.k[i] >= block_end: # 跳出当前块 self.step[i] = 1 self.next[i] = i + self.k[i] else: # 依赖块内后续位置 self.step[i] = self.step[i + self.k[i]] + 1 self.next[i] = self.next[i + self.k[i]] def query(self, x): # 查询弹跳次数 res = 0 while x < self.n: res += self.step[x] x = self.next[x] return res def update(self, x, y): # 修改弹力系数并重建块 self.k[x] = y block_start = (x // self.block_size) * self.block_size block_end = min(block_start + self.block_size, self.n) for i in range(block_end - 1, block_start - 1, -1): if i + self.k[i] >= block_end: self.step[i] = 1 self.next[i] = i + self.k[i] else: self.step[i] = self.step[i + self.k[i]] + 1 self.next[i] = self.next[i + self.k[i]] ``` #### 复杂度分析 - **预处理**:$O(n)$ - **查询**:$O(\sqrt{n})$ - **修改**:$O(\sqrt{n})$ #### 应用场景 分块法适用于需要**动态维护跳跃路径**且**支持快速修改**的问题,例如弹飞绵羊、树状路径跳跃等[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值