【洛谷P3146(USACO16OPEN)】248 G

文章介绍了如何使用区间动态规划(DP)解决一个与2048类似的游戏策略问题,其中合并规则是数值相等的相邻数字合并后加1。文章详细阐述了DP的五步法,包括状态表示、阶段划分、状态转移、边界设定和最终答案,并给出了状态转移方程和代码实现。此外,文章还提醒注意状态转移中的细节和可能的陷阱。

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

作者:乐天JOE
题目链接


题目描述

Bessie likes downloading games to play on her cell phone, even though she doesfind the small touch screen rather cumbersome to use with her large hooves.

She is particularly intrigued by the current game she is playing.The game starts with a sequence of N N N positive integers ( 2 ≤ N ≤ 248 2 \leq N\leq 248 2N248), each in the range 1 … 40 1 \ldots 40 140. In one move, Bessie cantake two adjacent numbers with equal values and replace them a singlenumber of value one greater (e.g., she might replace two adjacent 7s with an 8). The goal is to maximize the value of the largest numberpresent in the sequence at the end of the game. Please help Bessiescore as highly as possible!

给定一个1*n的地图,在里面玩2048,每次可以合并相邻两个(数值范围1-40),问序列中出现的最大数字的值最大是多少。注意合并后的数值并非加倍而是+1,例如2与2合并后的数值为3。


输入格式

The first line of input contains N N N, and the next N N N lines give the sequence

of N N N numbers at the start of the game.


输出格式

Please output the largest integer Bessie can generate.


样例 #1

样例输入 #1

4
1
1
1
2

样例输出 #1

3

提示

In this example shown here, Bessie first merges the second and third 1s to

obtain the sequence 1 2 2, and then she merges the 2s into a 3. Note that it is

not optimal to join the first two 1s.



思路

这一题需要用到区间dp这一个算法,不会的可以先移步这一题:石子合并——这是区间dp的模板题。

看到题目:在一个 1 × n 1 \times n 1×n的地图。说明什么,这是1个 一维序列 ,也就是1条 线

那用的算法就很容易能看出来了,用区间dp

至于原因,是因为区间dp是在一条线上进行的,而且更好进行合并的步骤。

由于是2048游戏,所以还有1个隐藏条件:合并的两个数必须是相同且相邻的。(这点在翻译中没有体现)


来考虑dp的五个步骤。

1.状态表示:

区间 [i,j]

dp[i][j]表示的是:序列第i个数,放到第j个数区间内,全部合并的最大值。

2.阶段划分

由于是区间dp,那么显而易见,实际上我也不确定

小区间–>大区间

3.状态转移

合并代价:+1

i为左端点,j为右端点,k为断点

if(当前数字和下一个数字相同)
    dp[i][j]=max(dp[i][j],dp[i][k]+1);

这是本来的状态转移方程,有什么问题吗?

有,那就是dp[i][j]是全部合并而且可以不用相邻的合并,所以答案有可能是零或比较大的数像下面这几组数据:

样例1

3
7 8 7

样例2

3
1 2 3

第一个按照上面的状态转移方程答案会是9,因为不是相邻的两个7会合并。而第二个会是0,因为不能全部合并。

那我们就用一个值(ans)用来存合并后区间[i,j]内的最大数字

那正确的状态转移方程就很好写了:

if(当前数字和下一个数字相同)
{
    dp[i][j]=max(dp[i][j],dp[i][k]+1);
    ans=max(ans,dp[i][j]);
}

4.边界(初始值)设定

很简单,只需要把每个数设定为一个小区间,那么全部合并的最大答案就是那个数。

每次输入x,答案应该是区间[i,i]为这个数,其他为0:

for(int i=1;i<=n;i++)
{
    cin>>x;
    dp[i][i]=x;
}

5.最终答案

首先想到的是dp[1][n],但看到状态转移的部分,dp[1][n]是全部合并,而我们耗费了那么多的精力去创造的ans不是正好适合吗?

那么最后输出的答案就是ans。


坑点

这题有一些坑点:

  • 状态转移中说过的ans
  • 左端点与右端点的加一减一要搞清楚,不然要调很久,亲身经历
  • 搞清楚每个循环中每个变量是干嘛的,不要直接套模板

最后就可以开开心心的写代码了!


code

最后看代码

#include<bits/stdc++.h>
#define int long long
#define itn int
using namespace std;
int n,x,dp[255][255],ans; 
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++) //初始化
	{
		cin>>x;
		dp[i][i]=x;
	}
	for(int len=2;len<=n;len++) //len是遍历长度的,不需要从1开始
		for(int i=1;i<=n-(len-1);i++) //左端点
		{
			int j=i+len-1; //右端点
			for(int k=i;k<j;k++) //断点
				if(dp[i][k]==dp[k+1][j]) //如果左右相等(可以进行合并)
				{
					dp[i][j]=max(dp[i][j],dp[i][k]+1); //合并(也可以是dp[k+1][j]+1)
					ans=max(ans,dp[i][k]+1); //更新答案
				}
		}
	cout<<ans; //输出区间[1,n]的最大数字
   return 0;
}

AC记录
本人是个蒟蒻,学艺不精。如果有错误或建议请在评论区指出。

十分感谢

<think>好的,我现在需要解决洛谷的P3612题目,题目名称是“Secret Cow Code”。首先,我得仔细看看题目要求。题目大意是说,给定一个字符串S和一个整数N,当字符串以特定的方式无限扩展时,找出第N个字符。扩展的规则是每次将当前字符串的最后一个字符移到最前面,然后将这个新字符串追加到原字符串后面。比如,原字符串是“COW”,第一次扩展变成“COW” + “WCO”,第二次扩展就是“COWWCO” + “OWC” + ... 这样不断进行下去。 接下来,我需要想如何高效地找到第N个字符,因为直接模拟扩展过程显然不可行,尤其是当N很大的时候,比如题目里提到的N可以是10^18,这样的数据规模必须用数学方法或者递归的思路来解决。 首先,理解扩展的规律。每次扩展后的字符串长度是原来的两倍减一,比如原长度是L,第一次扩展后是2L-1,第二次是2*(2L-1)-1=4L-3,依此类推。但实际题目中的扩展方式可能有所不同。或者可能每次将字符串的最后一位移到前面,然后拼接。例如,原字符串s,扩展后的字符串是s + rotated(s),其中rotated(s)是将s的最后一个字符移到前面后的字符串。比如COW变成COWWCO,因为原字符串是COW,最后一个字符是W,移到前面变成WCO,所以拼接后是COW + WCO = COWWCO,此时长度是原长度的两倍。但是原题可能每次扩展后的长度是原长度的两倍,而不是两倍减一?这点可能需要再仔细看题目描述。 不过根据题目例子,原字符串是“COW”,长度3。第一次扩展后变成“COWWCO”,长度6。然后第二次扩展变成“COWWCOOCOWWC”吗?或者可能每次扩展后的长度是前一次的两倍?比如原长度是L,每次扩展后的长度是L*2。例如,原长3→6→12→24...等。这样,当N大于当前长度时,可以通过递归的方式找到对应的位置。 假设每次扩展后的字符串长度是前一次的两倍减一?或者可能题目中的扩展方式是:每次将字符串s变为s + s[last] + s[0..last-1],也就是将最后一个字符移到前面,然后拼接。例如,原字符串s,扩展后的字符串是s + last_char + s[0..-2]。比如,COW的扩展是COW + W + CO → COWWCO。这时候,长度是原长度L → L + 1 + (L-1) = 2L。这样的话,每次扩展后的长度是原来的两倍。所以原字符串长度是L,第一次扩展后是2L,第二次是4L,以此类推。那么,当N超过原字符串长度时,可以递归地找到对应的位置。 比如,假设当前的长度是len,当N <= len时,直接返回s[N-1]。否则,我们需要找到在扩展后的字符串中的位置。例如,假设扩展后的字符串是原字符串的两倍长度,那么当N在扩展后的后半部分时,相当于原字符串的某个位置转换后的结果。比如,当原字符串长度是L,扩展后的长度是2L。假设N在L+1到2L之间,那么对应的位置可能是N - L -1的位置在扩展后的后半部分。或者可能需要通过不断将N减去当前长度的一半,直到找到在原始字符串中的位置。 例如,假设当前阶段的字符串长度是L,每次扩展后的长度是2L。那么,当N大于L时,我们可以考虑N所在的区间。比如,如果L < N <= 2L,那么N的位置对应的是扩展后的部分。而扩展后的部分是原字符串最后一个字符加上原字符串的前L-1个字符。例如,原字符串是s,扩展后的后半部分是s[-1] + s[0..-2]。因此,当N在L+1到2L的位置时,这个位置的字符对应于原字符串的位置的计算可能需要调整。 例如,当N是L+1的位置时,对应的是原字符串的最后一个字符。当N是L+2的位置时,对应的是原字符串的第0个字符(假设索引从0开始)。或者,可能需要将N减去L+1,然后递归处理。例如,当N处于后半部分时,N减去L+1得到的位置是k,然后递归处理k的情况。但需要考虑,每次扩展后的结构是否与原结构相似,从而可以用递归或迭代的方法找到对应的位置。 举个例子,假设原字符串是“COW”,长度L=3。扩展后的长度是6。此时,前3个字符是原字符串,后3个字符是“WCO”(将原字符串的最后一个字符W移到前面,然后加上原字符串的前两位CO)。当N是4的话,对应的位置是4-3-1=0?或者可能其他计算方式? 或者,另一种思路是每次扩展后的字符串长度为两倍原来的长度,例如,初始长度是L,扩展一次后是2L,再扩展是4L,依此类推。那么,当N大于当前长度时,我们需要找到当前所在的扩展层级。比如,找到最大的k,使得 L * 2^{k} < N。这可能比较复杂。或者,可以通过不断将当前长度翻倍,直到超过N,然后回溯处理。 具体来说,假设初始长度是len = original_length。我们不断将len乘以2,直到len >= N。此时,每次将len折半,并检查N是否处于后半部分。例如,当len是当前扩展后的长度,而原来的长度是len/2。如果N > len/2,那么这个位置对应于后半部分。此时,后半部分的第一个字符是原字符串的最后一个字符。然后剩下的部分对应于原字符串的前半部分。例如,当len是扩展后的长度,原字符串是s,那么后半部分的结构是 last_char + s[0..-2]。因此,当N处于后半部分的位置时,对应的位置可以转换为在前半部分的某个位置。 举个例子,假设原长度是3,扩展后的长度是6。如果N是5,处于后半部分(4-6的位置)。后半部分的结构是WCO。此时,位置5对应后半部分的第2个字符(从1开始的话,后半部分的起始位置是4,5对应第二个字符)。那么,后半部分的第一个字符是原字符串的最后一个字符W,之后是原字符串的前两个字符C和O。因此,5的位置对应的原字符串的位置是5 - (3) -1 =1,然后递归处理这个位置。或者,可能需要将N减去前半部分长度加一,得到k,然后递归处理k。 比如,假设当前长度是curr_len,当N > curr_len/2时,那么它处于后半部分。此时,如果N等于curr_len/2 +1,对应的字符是原字符串的最后一个字符。否则,N的位置对应于原字符串的位置k = N - (curr_len/2 +1),此时需要递归处理k,然后将结果对应的字符是原字符串k-1的位置,因为后半部分的结构是最后一个字符加上原字符串的前部分。 但这里可能需要处理一些边界情况,比如当k=0的时候,可能需要取原字符串的最后一个字符? 或者,更具体地说: 假设当前阶段的字符串长度是curr_len,每次扩展后的长度是curr_len * 2。那么,当需要找到N的位置时: - 如果 N <= original_len,直接返回s[N-1]。 否则,我们需要找到curr_len,使得 curr_len/2 < N <= curr_len。例如,当curr_len是第一个大于等于N的长度,那么进入循环: while curr_len < N: curr_len *= 2 然后,当curr_len >= N时,我们开始往回退: 每次循环: 如果 curr_len//2 < N <= curr_len: offset = N - curr_len//2 -1 # 因为后半部分的第一个字符是原字符串的最后一个字符,所以当N=curr_len//2+1时,offset是0,对应最后一个字符 if offset ==0: return s[-1] else: N = offset curr_len = curr_len//2 但这里可能需要不断重复这个过程,直到N落在原始字符串的长度范围内。 例如,假设原始字符串长度是3,N=5: 初始curr_len是3。需要扩展直到超过N。第一次扩展后是6,此时5<=6。进入循环: 检查是否 N > curr_len//2 (curr_len是6,curr_len//2是3). 5>3,所以进入后半部分。 offset =5 -3 -1=1. 然后,N=1,此时curr_len=3. 现在检查N是否在原始长度范围内。此时N=1 <=3,所以返回s[0](假设索引从0开始)。 所以结果就是原字符串的第一个字符,即C。但原例子中,扩展后的字符串是COWWCO,当N=5时,应该是后半部分的第5-3=2个位置(后半部分长度为3),即WCO中的第三个字符O?或者可能我的计算方式有问题? 或者,原字符串是COW,第一次扩展后的字符串是COW + WCO → COWWCO。长度是6。那么位置5(索引从1开始的话)是第5个字符,即O。而根据上面的方法计算得到的是原字符串的第0个字符C?这说明我的逻辑有问题。 这说明我的思路有问题,需要重新分析。 正确的扩展方式应该是每次将原字符串的最后一个字符移到前面,然后追加到原字符串后面。比如原字符串是s,长度为L。扩展后的字符串是s + (last_char + s[0..L-2]). 例如,原字符串COW(长度3),扩展后是COW + W(最后一个字符) + CO(前L-1个字符) → COWWCO,长度6。下一次扩展,将最后一个字符O移到前面,得到O + COWWC,然后追加到原字符串后面,得到COWWCOOCOWWC,长度12。 现在,假设我们要找N=5的位置,在第一次扩展后的字符串是COWWCO。各个位置(1开始)的字符是: 1:C, 2:O, 3:W, 4:W, 5:C, 6:O. 所以N=5对应的是C。但根据上面的方法,之前的计算得到的是原字符串的第1个字符(索引0)即C,这与实际结果一致。那之前的例子是正确的。 那这说明,当N=5时,通过递归计算得到的是正确的。那说明我的思路是正确的? 那需要再验证另一个例子。比如,第一次扩展后的字符串是COWWCO,长度为6。假设N=4,对应的字符是W。按照上述方法: curr_len=6,N=4. 判断是否4>3(curr_len//2=3) → 是。offset=4-3-1=0。所以返回原字符串的最后一个字符,即原字符串是COW,最后一个字符是W。所以结果正确。 那这时候,当offset=0时,取原字符串的最后一个字符。而如果offset>0,则新的N是offset,curr_len变为curr_len//2=3。此时,新的N是offset,比如假设原N=5,offset=1,则新的N=1,curr_len=3。此时N=1<=3,所以取原字符串的第0个字符C。正确。 那这说明上述的递归方法是正确的。 因此,算法的步骤可以总结为: 1. 计算原始字符串的长度len,记为original_len。 2. 找到最大的curr_len,使得 curr_len = original_len * 2^k,直到curr_len >= N。然后,不断将curr_len折半,调整N的位置,直到N落在原始长度范围内。 3. 每次当N处于后半段时,即N > curr_len//2,计算offset = N - (curr_len//2) -1。如果offset为0,则返回最后一个字符;否则,将N设为offset,curr_len设为curr_len//2,继续处理。 具体来说,算法的步骤如下: - 初始化curr_len为原始字符串的长度。 - 将curr_len不断翻倍,直到它大于等于N。 - 然后,进入循环,每次处理当前curr_len: while curr_len > original_len: curr_half = curr_len // 2 if N > curr_half: offset = N - curr_half - 1 if offset == 0: N = curr_half else: N = offset curr_len = curr_half - 最后,返回原始字符串的第N-1个字符(因为索引从0开始)。 或者,可能需要更仔细的处理,比如: 例如,当curr_len是当前处理的总长度,如果N > curr_half,则进入后半部分。此时后半部分的第一个字符是原字符串的最后一个字符。剩下的部分是原字符串的前部分。例如,后半部分是last_char + s[0..length-2]。所以,当N等于curr_half +1时,对应的就是last_char。否则,当N在curr_half+2到curr_len时,对应的位置是原字符串的N - (curr_half +1) -1的位置? 或者,可能需要将N转换为对应的位置: 假设原字符串是s,长度L。扩展后的长度是2L。后半部分的结构是last_char + s[0..L-2]. 所以,当在后半部分的位置是p(相对于后半部分的起始位置),则对应的原字符串的位置是p-1(如果p>0的话)。 比如,当N在后半部分的位置是curr_half +1 →对应的是last_char。当N是curr_half+2 →对应的是s[0], curr_half+3→s[1], 以此类推。 因此,当N在后半部分时,我们可以得到: 如果 N == curr_half +1 →返回s的最后一个字符。 否则,新的N等于N - (curr_half +1) →得到后半部分中去掉第一个字符后的位置,即相当于在原字符串的前L-1个字符中的位置。所以新的N' = N - (curr_half +1)。然后,curr_len变为curr_half(原长度的一半?或者原长度是L,curr_half是L,新的长度是L-1?这可能有问题。) 或者,或许更正确的方式是,每次将curr_len折半,然后处理: 当N在后半部分时: 如果 N == curr_half +1 →返回最后一个字符。 否则,N = N - (curr_half +1),然后 curr_len = curr_half -1。因为后半部分的结构是last_char + s[0..curr_half-2]。所以,此时剩余部分的长度是 curr_half-1。例如,原长度是L=3,curr_half=3,后半部分的结构是last_char(W) + s[0..1](CO),总长度3。所以,当N在后半部分且不是第一个位置的话,新的N是N - (curr_half +1) →比如,当原N是5(curr_half=3),5-3-1=1,所以新的N=1,此时curr_len=3-1=2?或者可能需要调整。 这个思路可能需要更仔细的分析。例如,原字符串长度L=3,扩展后的长度是6。curr_half=3。后半部分的长度是3。结构是W(last_char) + CO(前两位)。所以,当N在4到6的位置时: N=4 →对应W,即原字符串的最后一个字符。 N=5 →对应C,即原字符串的第一个字符。 N=6 →对应O,原字符串的第二个字符。 所以,当处理N=5时,需要将N调整为5 -3 -1=1,然后此时curr_len=3,但原来的字符串长度是3,所以这时候新的curr_len应该变为2(因为后半部分的结构是W + CO,即长度为3,其中后两位是前两位的原字符串字符)。 所以,每次处理时,当N在后半部分: 如果N == curr_half +1 →返回最后一个字符。 否则,N = N - (curr_half +1),此时新的curr_len应该是curr_half -1,因为剩下的部分是原字符串的前curr_half-1个字符。例如,原curr_half是3,剩下的部分是CO,长度2。所以新的curr_len是2。 但这样的话,curr_len在每次处理时会逐渐减小,直到等于原字符串长度? 或者,可能这个思路需要将curr_len在每次处理时调整为curr_half-1。例如: 当处理curr_len时,如果N > curr_half: if N == curr_half +1: 返回最后一个字符 else: N = N - (curr_half +1) curr_len = curr_half -1 这样,当原curr_len是6,处理N=5: curr_half=3 →5>3 →检查是否等于4?不是。N=5-3-1=1 →新的N=1,curr_len=3-1=2. 现在,再次处理: curr_len=2,curr_half=1. 此时,N=1 <=1 →返回原字符串的第0个字符? 但原字符串是COW,而这里curr_len被调整为2,这可能不正确,因为原字符串的长度是3,而这里在递归过程中curr_len可能变成了2? 这可能说明这种处理方式有问题。需要重新考虑。 可能正确的处理方式是,每次扩展后的字符串长度是前一次的两倍。例如,初始长度是L=3,第一次扩展后是6,第二次是12,依此类推。此时,每次的curr_len是这些长度。当处理时,如果N在后半部分,那么我们需要找到对应的位置。 例如,当curr_len=6,处理N=5: curr_half=3. 因为5>3,所以进入后半部分。后半部分的结构是原字符串的最后一个字符(W)加上原字符串的前两个字符(CO)。 所以,后半部分的长度为3,其中第一个字符是W,后面两个是C、O。 此时,当N=5,即在后半部分中的位置是5-3=2(相对于后半部分的起始位置是4,即第4、5、6三个位置,N=5是后半部分的第二个位置)。所以,对应的字符是后半部分中的第二个字符,即C。 那么,如何将N=5转换为原字符串中的位置? 后半部分的结构是:W(位置4),C(位置5),O(位置6)。 如果N=5,对应的后半部分中的索引是1(从0开始),那么对应的原字符串中的位置是0(因为后半部分的结构是W + s[0] + s[1])。 因此,当N在后半部分时,计算方式应为: 如果N在后半部分,那么计算相对位置为N - curr_half。如果相对位置是0,则对应的是原字符串的最后一个字符。否则,相对位置减1得到原字符串中的位置。例如: N=4 →相对位置0 →原字符串最后一个字符。 N=5 →相对位置1 →原字符串的位置0. N=6 →相对位置2 →原字符串的位置1. 这样,当处理N在后半部分时,可以通过以下方式调整: offset = N - curr_half if offset ==0: new_N = curr_half -1 (因为原字符串的长度是curr_half,最后一个字符的索引是curr_half-1) else: new_N = offset -1 然后,将curr_len设为curr_half,继续处理新的_N。 例如,curr_half是3: 当N=5,offset=5-3=2. new_N=2-1=1. curr_len=3. 现在,再次处理N=1,curr_len=3(原字符串长度是3)。此时N<=3,返回s[0]. 所以,得到的字符是C,正确。 另一个例子,N=4: offset=4-3=1 →因为offset不是0,所以new_N=1-1=0. curr_len=3. 此时N=0?或者可能我的索引方式有问题? 或者,当处理时,原字符串的索引是0-based,而N是1-based? 例如,原问题中的N是1-based的。比如,原字符串的字符位置是1到L。 因此,当转换到原字符串中的位置时,需要保持1-based。 例如,原字符串的长度是3,字符是1:C, 2:O,3:W. 当处理N=4时: curr_half=3 →offset=4-3=1. 因为offset>0 →new_N=1-1=0?或者可能我的转换方式错误? 或者,此时在后半部分的结构是:位置4是W(原字符串的3),位置5是C(原字符串的1),位置6是O(原字符串的2). 所以,当N=5(对应后半部分的第二个位置),转换后的新N是1,即原字符串的第一个位置,返回C。 当N=4(后半部分第一个位置),转换后的new_N是0?或者,这时候,当offset=1,new_N是offset-1=0,此时在curr_len=3时,N=0是不正确的,因为N应该是1-based的。 这说明,我的转换逻辑存在错误。需要重新分析。 正确的转换方式可能应该是: 当N在后半部分时: - 后半部分的第一个字符是原字符串的最后一个字符(位置L)。 - 后半部分的第二个字符是原字符串的第一个字符(位置1)。 - 后半部分的第三个字符是原字符串的第二个字符(位置2)。 依此类推,直到填满后半部分。 所以,当N在后半部分的位置为k(相对于后半部分起始位置是curr_half+1),则对应的原字符串的位置是: 如果k=1 →最后一个字符(原位置L)。 否则,k>1 →对应的原字符串位置是k-2(因为后半部分的第二个字符对应原字符串的第一个字符)。 或者,可能需要将N转换为在原字符串中的位置。例如: 在后半部分,总共有curr_half个字符。其中第一个字符是原字符串的最后一个字符,剩下的curr_half-1个字符是原字符串的前curr_half-1个字符。例如,原字符串长度是L=3,后半部分的长度是3,结构是W(原3),C(原1),O(原2)。 因此,当N在后半部分中的位置是相对于curr_half+1的,比如: N=4 →后半部分的第一个字符 →原3. N=5 →后半部分的第二个字符 →原1. N=6 →后半部分的第三个字符 →原2. 所以,当N处于后半部分时,可以计算: k = N - curr_half. 如果k ==1 →原字符串的最后一个字符(位置L). 否则,k>1 →原字符串的位置是k-1 -1 =k-2?或者可能是k-1的位置? 或者,k的取值是1到curr_half: 比如,当curr_half=3,后半部分的k=1对应原3,k=2对应原1,k=3对应原2. 所以,当k=1 →原L. 当k>1 →原k-1 -1?或者原k-2? 或者,可以发现,当k>1时,对应的是原字符串的k-2的位置? 例如,k=2 →原0(假设0-based) →C? 这显然有问题,因为原字符串的0-based是C(索引0)。 所以,可能正确的转换方式是将k>1时,对应的原位置是(k-2) % (L-1),其中L是原字符串的长度? 或者,这可能比较复杂。另一种思路是,当处理N在后半部分时: 如果k=1 →原L-1(0-based). 否则,新的N =k-2,并在原字符串的前L-1个字符中查找。 例如,原字符串长度是3,后半部分的k=2对应原字符串的0,k=3对应原字符串的1. 这样,当处理N=5时: k=5-3=2 →新的N=2-1=1 →原字符串的索引0. 这可能更合理。 这似乎比较复杂,所以需要找到一个通用的方法,将N在后半部分的位置转换为原字符串中的位置。 正确的做法可能是在每次处理时,当N在后半部分时: 1. 如果N等于curr_half +1 →返回原字符串的最后一个字符。 2. 否则,将N设置为N - (curr_half +1),然后继续处理,此时curr_len变为curr_half -1. 比如,原字符串长度是3,curr_half=3,N=5 →5 -3-1=1 →新的N=1,curr_len=3-1=2. 此时,curr_len是2,而原字符串的长度是3,这显然有问题,因为curr_len应该指的是扩展后的字符串长度。这似乎不正确。 或者,可能需要保留原字符串的长度不变,而只调整当前处理的长度。例如,每次扩展后的长度是原来的两倍,而原字符串的长度始终是original_len。所以,当处理到某个阶段,curr_len是扩展后的长度,当N在后半部分时,需要将其转换为原字符串的结构。 这可能意味着,每次调整时,curr_len是当前扩展后的长度,而原字符串的长度保持不变。因此,当处理到后半部分时,新的N需要调整为对应到原字符串的前L-1个字符中的位置。 这可能需要更仔细的数学推导。 假设原字符串的长度是L。每次扩展后的长度是2^k * L,其中k是扩展次数。例如,扩展一次后长度是2L,两次是4L,等等。当N处于某个扩展后的长度中,即处于区间 (2^{k-1} L, 2^k L]时,需要找到对应的位置。 此时,每次处理时,我们需要将当前的curr_len(当前扩展后的长度)除以2,得到之前的扩展后的长度。然后,判断N是否在后半部分。如果是,则需要调整N。 例如,当curr_len=2^k L时,前半部分的长度是curr_half=2^{k-1} L. 后半部分的长度也是curr_half. 但根据扩展规则,后半部分的结构是原字符串的最后一个字符,加上原字符串的前curr_half-1个字符。因此,当N处于后半部分时,对应的位置可以通过以下步骤计算: 假设当前curr_len=2^m L。当前处理的是第m次扩展后的字符串。当N在后半部分时: 位置在curr_half+1到curr_len之间。 对于这些位置,第一个字符是原字符串的最后一个字符。后续的字符对应于原字符串的前curr_half-1个字符,但每次扩展后的结构可能递归地应用同样的规则。 因此,处理的方法可以是: 当N>curr_half时: 如果N == curr_half +1 →返回原字符串的最后一个字符。 否则,调整N为N - (curr_half +1),然后curr_len变为 curr_half -1。 因为后半部分的结构是最后一个字符,加上原字符串的前curr_half-1个字符,所以此时相当于进入一个长度为curr_half-1的新字符串。 这可能不正确,因为原字符串的长度是固定的,而每次扩展后的长度是前一次的两倍。所以,当调整curr_len时,应该将其设为 curr_half,而不是 curr_half-1. 或者,可能这部分的处理需要更仔细的调整。例如: 当curr_len=2^m L,curr_half=2^{m-1} L. 当N>curr_half: if N == curr_half +1 →返回原字符串的最后一个字符。 else →新的N是 N - (curr_half +1), 并且curr_len = curr_half -1. 这似乎会导致curr_len减少到curr_half-1,这可能与原扩展后的长度不匹配。例如,当原扩展后的长度是2^m L,而curr_half=2^{m-1} L。此时,curr_half-1=2^{m-1} L -1,这可能不是扩展后的有效长度。 这表明,或许我的思路有错误,需要重新考虑。 另一种思路是,将问题看作递归的。例如,当N处于扩展后的后半部分时,可以通过不断减小N的方式,找到其在原字符串中的对应位置。 例如,初始字符串长度为L。每次扩展后,字符串长度翻倍。当N大于L时,我们可以逐步将N减少到L范围内。 具体来说,假设当前扩展后的字符串长度是L*2^k。我们需要找到k,使得L*2^{k-1} < N <= L*2^k. 然后,处理N: 当N > L*2^{k-1}: offset = N - L*2^{k-1} if offset ==1: return s[-1] else: new_N = offset -1 然后,处理new_N,其中现在k减少1,即新的扩展次数是k-1,当前扩展后的长度是L*2^{k-1}. 重复这一过程,直到新的_N落在L的范围内,然后返回s[new_N-1]. 例如,原字符串长度L=3,N=5. 初始扩展后的长度是6(k=1,因为 3*2^1=6). N=5 >3*2^0=3. offset=5-3=2. 因为 offset !=1,所以 new_N=2-1=1. 现在,处理new_N=1,此时扩展次数k=0,字符串长度3. 1<=3,返回s[0],即C。正确。 另一个例子,N=4: offset=4-3=1 →返回s[-1]即W。正确。 再比如,N=6: offset=6-3=3 →new_N=3-1=2 →处理new_N=2,返回s[1]即O。正确。 这表明这种方法是可行的。所以,算法的大致步骤如下: 1. 计算原字符串的长度L。 2. 如果N <=L,直接返回s[N-1]. 3. 否则,找到最大的k,使得 L*2^{k} < N。或者,可以不断将当前长度翻倍,直到超过N,然后回溯处理。 但更高效的做法是,用循环来模拟这一过程: 初始化当前长度curr_len为L. while curr_len < N: curr_len *=2 然后,进入循环: while curr_len > L: curr_len_half = curr_len //2 if N > curr_len_half: offset = N - curr_len_half if offset ==1: N= curr_len_half else: N=offset-1 curr_len = curr_len_half 最后,返回s[N-1] 这似乎可以得到正确的结果。 例如,测试原字符串COW,L=3: N=4: 初始化curr_len=3→3<4,翻倍到6. 进入循环: curr_len=6>3. curr_half=3. N=4>3 →offset=4-3=1 →等于1 →N=3. curr_len=3. 循环结束。返回s[3-1] →s[2]=W。正确吗?原字符串是COW,s[2]是W,而N=4对应扩展后的字符串的第四个字符,即W。正确。 另一个例子,N=5: curr_len初始3→翻倍到6. 处理curr_len=6: N=5>3 →offset=5-3=2 !=1 →N=2-1=1. curr_len=3. 返回s[0]=C。正确。 N=6: curr_len=6 →处理: offset=6-3=3 →!=1 →N=3-1=2 →返回s[1]=O. 正确。 这说明这个算法是正确的。 因此,C语言代码的大致结构是: 读取字符串s和N. 计算L = strlen(s). 如果N <=L →输出s[N-1]. 否则: curr_len = L; while (curr_len < N) curr_len *=2; while (curr_len > L) { curr_half = curr_len/2; if (N > curr_half) { offset = N - curr_half; if (offset ==1) { N = curr_half; } else { N = offset -1; } } curr_len = curr_half; } printf("%c", s[N-1]); 需要注意的边界条件包括: 1. 当N等于curr_half+1时,即offset=1,此时N被设置为curr_half,这可能超过原字符串的长度吗?例如,当curr_half是原字符串的长度,此时将N设置为curr_half等于原字符串的长度。例如,原字符串长度是3,curr_half=3,N=4→offset=1→N=3,此时curr_len=3,判断curr_len>3不成立,返回s[2]. 所以是正确的。 另一个测试案例,原字符串长度为1,比如s="A",N=2. 处理: L=1. curr_len=1→翻倍到2. 进入循环: curr_half=1. N=2>1 →offset=2-1=1 →N=1. curr_len=1. 退出循环,返回s[0],即A。这正确吗? 扩展后的字符串是"A" →第一次扩展后的字符串是A + A的前半部分?或者原字符串是A,扩展后的字符串是AA?原扩展规则是将最后一个字符(A)放到前面,然后追加到原字符串后面,所以扩展后的字符串是A + A →AA。所以N=2的字符是A。正确。 另一个例子,原字符串是ABCD,长度4。第一次扩展后变成ABCD DABC(假设最后一次字符是D,移到前面,所以新部分是 DABC →ABCDDABC,长度8. 假设N=5→D,N=6→A,N=7→B,N=8→C. 测试N=6: L=4. curr_len=4 →翻倍到8. 处理: curr_half=4. N=6>4 →offset=6-4=2 →offset !=1 →N=2-1=1. curr_len=4. 退出循环,返回s[0]即A. 正确吗? 原字符串是ABCD,扩展后的字符串是ABCDDABC。N=6对应的字符是A,正确。 所以算法正确。 综上所述,C语言的实现需要按照上述步骤进行。</think>要解决洛谷P3612题“Secret Cow Code”,我们需要找到按特定规则无限扩展后的字符串中第N个字符。由于直接模拟扩展过程不可行,需采用递归或迭代方法高效定位字符。 ### 题目分析 扩展规则:每次将当前字符串的最后一个字符移到最前面,然后将这个新字符串追加到原字符串后面。例如,原字符串为“COW”,第一次扩展后为“COW” + “WCO” → “COWWCO”。 ### 关键思路 1. **长度倍增**:每次扩展后字符串长度变为原来的两倍。 2. **递归定位**:当N超过当前长度的一半时,调整N至前半部分对应的位置,直至N位于原字符串范围内。 ### 算法步骤 1. **初始化**:计算原字符串长度`L`。 2. **扩展长度**:找到最小的扩展后长度`curr_len`,使其不小于N。 3. **逆推定位**:逐步将`curr_len`折半,调整N的位置: - 若N在后半部分,计算其在前半部分的对应位置。 - 若N恰好在后半段的第一个位置,对应原字符串的最后一个字符。 ### 代码实现 ```c #include <stdio.h> #include <string.h> int main() { char s[31]; // 题目保证字符串长度不超过30 long long N; scanf("%s %lld", s, &N); long long L = strlen(s); long long curr_len = L; // 扩展curr_len直到超过N while (curr_len < N) { curr_len *= 2; } // 逆推定位N while (curr_len > L) { long long half = curr_len / 2; if (N > half) { long long offset = N - half; if (offset == 1) { N = half; } else { N = offset - 1; } } curr_len = half; } printf("%c\n", s[N - 1]); return 0; } ``` ### 代码解释 1. **输入处理**:读取原字符串s和目标位置N。 2. **长度扩展**:通过不断倍增`curr_len`,找到首个不小于N的长度。 3. **位置逆推**: - 当N位于后半段时,调整N至前半段的对应位置。 - 若N在后半段的第一个位置(即`offset == 1`),则直接定位到原字符串的最后一个字符。 4. **输出结果**:最终N落在原字符串范围内,直接输出对应字符。 ### 示例分析 以原字符串“COW”和N=5为例: 1. 初始长度L=3,扩展后`curr_len=6`(3→6)。 2. N=5 > 3(`half=3`)→ `offset=5-3=2` → N调整为`2-1=1`。 3. `curr_len`折半为3,此时N=1 ≤3,返回s[0]即‘C’。 该算法通过数学推导高效定位字符,避免了暴力扩展,时间复杂度为O(log(N/L))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值