【第九题】礼物(北理工/北京理工大学/程序设计方法与实践/小学期)

目录

前言

题干:【礼物】

测试用例:

心路历程

1 自己尝试TLE

2 论坛无果

3 柳暗花明

子序列自动机怎么学?

1 为什么要子序列自动机?

2 学习资料

3 补充思路

代码



前言

本文面向无竞赛或者算法基础的人,或者说,当你来搜这个题目的时候,我就默认你没有相关基础了。为什么写这篇博客呢?其实我觉得我是不配的,但是csdn上我这篇大概将会成为唯一一篇这道题的题解。(之前是有一篇的,但是因为题目有一些变化,所以那篇文章已经可以说是过时了)

建议顺着把文章看完,我个人理解,如果可以跳过前面直接到后面,那也不会面向CSDN编程了。

本人平平无奇,所以不会出现大佬们的视角盲区,各位平起平坐,放心食用,不用担心看不懂之类的。

我的基础具体说,只有假期自己看了300页c primer plus的基础,不会c++,不会STL,不会算法,只会c语言基础语法一把梭,大家可以对接一下自己的水平,我觉得这就是个中等水平。

我想说的是,不会可以学,不会并不代表不能会,英雄不问出处,请坚持住!

这道题搞了我几个小时,终于是完美地AC了,现在回头敲出来不会花很长时间,其实时间都花在学习新知识上了,下一道题根据@贝贝今天AC了吗 的文章,还要学数据结构的栈,头秃,就当是提前学吧,反正这小学期就是个提取学习,见到啥学啥。

本题解不是最优解,好在思路比较清晰,能稳定通过全部案例。


题干:【礼物】

Description

小张的好朋友小松要过生日了,小张打算为他挑选一件礼物。在市场上他发现有一个珠子手镯的商店很不错。在这家商店会出售特殊的珠子并穿成一个手镯,在货架上珠子排成一排,每一个珠子上有一个小写英文字母。店家有一个特殊的规定,必须在一排珠子中按顺序从左到右挑选。小张心中已经有一个想要送给小松的单词,请你告诉他应该如何挑选珠子使得手镯上珠子的字母组成小张想要的单词。

Input

第一行,一个字符串,表示货架上的一排珠子,仅包含小写英文字母,长度在200000以内。

第二行,一个字符串,表示小张想要的单词,仅包含小写英文字母,长度在10000以内。

Output

输出一行整数,表示小张按照从左到右需要挑选的珠子在货架上的位置。

Notes

从左到右按顺序选出的珠子上的字母为'p','p','y','h','a'。串成环形的手镯后可以组成"happy"。

数据保证有解,若有多种选取方法,输出其中字典序最小的一个,比如1 2 3 4比1 3 4 5的字典序小

提交后查看结果页面错误信息一栏,前4行的编译错误大家不用理会,第5行是关于你的结果的信息。

测试用例:

input

        pxrtpsapyjhuvab

        happy

output

        1 5 9 11 14


心路历程

1 自己尝试TLE

其实这道题乍一看很简单,不就是遍历吗。

然而TLE路上等你,说多了都是泪。

2 论坛无果

论坛的信息太过零散,大多是一种个人总结,带有装逼性质的那种,顺便混点分。这种不会讲太细,看不懂是很正常的。比如下面这个,现在回头看,他说的都是非常正确的,而且是很关键的,但是当时怎么就看不懂呢?关键出在基础知识上。

3 柳暗花明

无意间看到有人说子序列自动机,我依稀想起舍友之前和我说过这个东西。

当时舍友和我说的时候我上csdn看了看,感觉太难了,就没再看过,结果最后碰的头破血流,回头发现当时看起来最难的东西,可以说几乎是做出这道题AC的唯一途径,否则就是TLE。

于是就开始漫长的子序列自动机的学习。


子序列自动机怎么学?

1 为什么要子序列自动机?

逐字符遍历耗时太久,我们需要跳跃。

跳到哪里呢?比如我要找happy,我现在已经找到了h,那我能不能直接跳到后面第一个a呢?可以,只要我们储存了那个a的编号就可以,子序列自动机应运而生。

2 学习资料

超级简单的序列自动机-处理子串匹配_zzu.zzysleep-CSDN博客

这篇文章因为有图解,所以比较清晰易懂,但是有一个重大缺陷,那就是这个程序会跑错(破防),也就是说,她的思想是对的,但是有细节错误,所以这篇文章就供大家建立对子序列自动机的基本认识:横坐标代表什么?纵坐标代表什么?如何根据给出的带筛选字符串构造一个子序列自动机二维数组?如何用取出来的一种环(比如appyh)去跳跃遍历这个子序列自动机?

剩下的文章,大家也可以搜着看,核心都是一样的,就是建立自动机,然后遍历。

3 补充思路

其实要看懂自动机,最好还是有个思路在前。如果没有一个宏观思路,那么去看代码很容易只见树木不见森林,这里提供一下宏观思路:

首先要遍历所有可能的组合,类似:happy,appyh,直到yhapp

我们可以使用happyhappy字符串,从h到y每次顺次读取5个字符就可以实现,如果不想扩展字符串也可以用%运算来实现模仿环形。

对于每一种可能的5个字符,要在自动机中遍历,自动机的作用简单说就是加快遍历的速度,从一个一个遍历,到跳跃遍历。

好,如果能成功遍历到我们目标的5个字符,那么这个组合就是有解的。

之后就要判断是否是最优解了,这里要用一个ans数组储存最终的答案,用temp数组去做缓冲。这里涉及到一个字典序的的定义,字典序大家百度一下,其实就是类似于strcmp函数,只不过我们比较的内容超出了char范围,所以手写一个对int生效的“intcmp”函数,如果temp的字典序小于ans,那么就是更优解,那么就把temp写入ans,以此类推。

最后输出ans即可,因为不断优化,最后剩下的就是最优解了。

好的,到这里,就把大致的思路介绍完了,那么之后无论是学习子序列自动机或者看我的代码,都可以比较容易的看懂。


代码

如果你真的想学会这个知识点的话,建议你看懂我的代码后自己敲出来,或者对着我的代码先敲一遍,一边敲一边打注释,来辅助理解,最后删掉再自己敲出来,那就算是学会了。

我加了很多注释,这些注释其实是我在学习过程中记录的思考,同时大大提高程序可读性,大概csdn上像我这样几乎一句一个注释的二五仔少得很吧,但是为了理解,脸可以先放一边。

#include<stdio.h>
#include<string.h>
#include<stdbool.h>
#include<limits.h>
#define XLEN 200010
#define WORDLEN 10010
//上面两个都稍微开大点,不会越界
char x[XLEN]; //x储存待筛选字符串
bool used[XLEN];//used用来记录字符是否已经被使用过
char words[WORDLEN * 2]; //words开两倍,为了放两倍
int next[XLEN][26];//next是灵魂,子序列自动机
int ans[WORDLEN]; //ans是最优解
int temp[WORDLEN]; //temp是解
int main(void)
{
	int len_x, len_word;
	int i,j; //ijk用来计数循环
//	freopen("input.txt", "r", stdin); //0 测试重定向

	memset(next, -1, sizeof next); 
	scanf("%s%s", x, words);
	len_word = strlen(words);
	len_x = strlen(x);
	for (i = 0; i < len_word; i++)//初始化字典序,用最大字典序
		ans[i] = INT_MAX;
	strncat(words, words, 2*WORDLEN-len_word-1); //以双倍链代环
	//构造序列自动机
	//next记录的是从自己当前位开始以后的真实位置
	//i从strlen-1到0,不断继承上一个,继承后把自己所在的位置更新一下即可
	//因为数组故意开大,所以不会越界
	//其实我这里纠结过当前next应不应该包括自己,后来选择了包括
	//其实也有不包括的做法
	for (int i = strlen(x)-1; i >= 0; i--) 
	{
		for (int j = 0; j < 26; j++) 
			next[i][j] = next[i+1][j];
		next[i][x[i] - 'a'] = i;
	}
	//对所有环做遍历
	for (i = 0; i < len_word; i++)
	{
		int node = 0; //对一个环的情况,从头遍历到尾部
		int k = 0;//k用来辅助存放位置
		bool isloop = true; //每次假设可以成环
		bool isbetter = true; //假设每次都是更优解
		memset(used, 0, sizeof used); //每次都要刷新使用情况
		for (j = 0; j < len_word; j++)
		{
			if (used[node]) //修正保留当前位置带来的影响
				node++;
			node = next[node][words[i + j] - 'a'];
			temp[k++] = node + 1;//+1就存放了实际位置
			used[node] = true;
			if (node == -1) //后面没有了
			{
				isloop = false;
				break;
			}
		}
		if (isloop) //成功走完一圈,temp已经装好,计算最优解
		{
			//比较字典序,因为用的INT_MAX,第一次比较必定成功
			for (k = 0; k < len_word; k++)
				if (temp[k] > ans[k])
				{
					isbetter = false;
					break;
				}
				else if (temp[k] < ans[k]) 
					break;
			//必须加提前退出机制,否则就比如 temp 2 5 9 10和  ans 2 6 8 9
			//虽然没有提前退出,导致9>8反而判定字典序小
			//这个问题是基础不牢,比较字典序应该是基本功,分成三种情况
			//一个大于另一个就直接判断出来,相等才进行下一步
			//而不是把另外两种情况轻率的合并
			if (isbetter)//是更优解则写入ans
				for (k = 0; k < len_word; k++)
					ans[k] = temp[k];
		}
	}
//	printf("最优解:"); //3 测试
	for (i = 0; i < len_word; i++)//本来想用k的,毕竟k含义就是存位置的,但是k被释放了
		printf("%d%c",ans[i],i==len_word-1?'\n':' ');

	return 0;
}

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦梦亦醒乐逍遥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值