为防止广告,目前nocow只有登录用户能够创建新页面。如要创建页面请先登录/注册(新用户需要等待1个小时才能正常使用该功能)。

USACO/hidden

来自NOCOW
跳转到: 导航, 搜索
 这篇文章可以证实是由NOCOW用户原创,不存在任何版权争议。
 本文作者同意以GNU FDLCC-by-saGNU LGPL(如果适用)三种版权发布此文章(不包括翻译文章中属于原始所有者的部分版权)。
 如果你修改了这篇文章并且不同意用GNU FDL以外的版权发布,可以换一个版权模板或者移除此模板。
这是USACO Chapter 5 .5中的OI题目Hidden Passwords介绍及题解,参见 翻译C语言代码C++语言代码Pascal语言代码


目录

[编辑] 问题分析

首先,为了解决环的情况,可以在这个字符串后面再接一个相同的字符串。这样不影响结果。 本题可以采用比较法。 我们可以用两个标记i、j表示两个待比较字符串(长度为n)的起始位置。初始时i=0,j=1。 每次比较以i开头和以j开头的字符串s[i]和s[j]的大小,如果s[i]>s[j]则i=j,否则j向后移k位,k表示s[i]和s[j]的最长相同前缀的长度,因为s[i]的前K位的字母都大于等于s[i]的第一个字母,而s[i]与s[j]的前K为字母相同,所以下一次只需比较s[i+k]与s[i]即可,k在比较大小时顺便可以求出。不停比较直到j≥n。 这种算法可以应付大多数数据,但是因为最坏情况下算法的时间复杂度是O(n^2)的,对于特殊构造的数据就不行了。不过USACO没有出这种数据,放心做吧。

几个需要注意的细节:

  • 当比较时发现两个字符串相同时,直接输出0
  • k的最小值应为1

//不应该吧,如果原字符串是abaaba按照你的方法输出是0而我认为正确的应该是2…只是发表一下个人意见而已:我认为如果s[i]>s[j]则i=j,j=j+1.不然就j=j+k。 By TtXu

//LS貌似是算错了吧,按照上面的方法运行出来就是2,参见pascal程序1。 From Error


  • 本题为 最小表示法 问题.

(转自sevenlunar 和讯博客) 首先我们明确一个概念:最小后缀(least suffix)。

对于给定的一个串,将它的所有后缀按字典序排序,其中第一个后缀就是最小后缀。

那么如何求最小后缀呢?

最显然的方法是O(n2)的,这里我介绍一种O(n)的算法。

对于一个长度为n的主串s,设v[i]表示子串s[i..i+v[i]-1],该子串的所有前缀在主串中所有相应长度的子串中字典序最小(这句比较绕,但是很关键一定要弄明白)。

比如对于串acab则v[3]=2,即子串'ab'的所有前缀('a','ab')中,'a'在所有长度为1的子串('a','c','b')中字典序最小,'ab'在所有长度为2的子串('ac','ca','ab')中字典序最小。

我们要求的是最大的v[i],则i开始的主串的后缀就是最小后缀。

初始时v为0。设list记录当前所有的i,使得v[i]当前最大,初始时list=(1..n)。

对于每个在list中的i,

如果v[i+v[i]]>0,那么把s(i..i+v[i]-1)和s(i+v[i]..i+v[i]+v[i+v[i]]-1)合并后的s(i..i+v[i]+v[i+v[i]]-1)肯定最小,则v[i]=v[i]+v[i+v[i]];

同时为了避免重复,把i+v[i]从list中去掉。

如果v[i+v[i]]=0,那么当且仅当s[i+v[i]]在所有s[j+v[j]](j在list中)中最小时,v[i..i+v[i]]才最小,则v[i]=v[i]+1。

求出v后,把当前v最大的i放入list,其余的删掉,重复这个过程,直到list中只有1个i为止。

可以看出以上过程实际上就是不断地合并区间,而每个位置最多只访问一次,list中的位置最多删除一次,所以总的复杂度是O(n)的。


现在我们回到这道题上来,我们把主串复制两份,这样就解决了圈的问题。然后我们在串的最后加一个大于串中所有字符的字符('z'+1)。

此时这个串的最小后缀所对应的开始位置i就是所求。

这是很显然的,因为最小后缀的前缀肯定是最小的。


ps.这道题Maigo的程序从编程复杂度上比这个简单多了,但是其实是可以设计出数据TLE的(39999个b+1个a),只不过USACO和ZOJ都没有出这种数据。

ps.其实本题答案不应该仅仅原串复制后的最小后缀,因为题目中有要求出现位置最小,例如aaa应该输出0,但是按照上面那个方法求出的会是2

ps.对于上一个ps,复制以后增加一个比'z'还大的字符在串尾即可



又一个方法: 类似后缀数组,可在NlogN时间内求出子串任意长度的任意排名。 首先把字母复制一遍,处理后缀数组,得到2^k(k=0..log(n))的子串(后缀)的名次,然后: 字符串的长度N可以表示为二进制,位数不超过log(n),然后用类似后缀数组的倍增,把长度 N分成的块(每个块都是2^k),按照第一第二关键字,逐渐合并,每次合并用基数排序,O(N), 所以也是NLogN. 不过常数可能较大。

后缀数组是可行的,然而USACO又增加了数据(准确说改了题目),使得“aaaa”合法,那么使用完后缀数组后,我还用了kmp,不知神牛有无解决方法

              (我觉得可以增加串尾符啊……{by Tearhap_断章})

我不明白LS的意思,我觉得用后缀数组做很水。。。。{by Morenan} Executing...

  Test 1: TEST OK [0.011 secs, 2652 KB]
  Test 2: TEST OK [0.011 secs, 2652 KB]
  Test 3: TEST OK [0.011 secs, 2652 KB]
  Test 4: TEST OK [0.011 secs, 2652 KB]
  Test 5: TEST OK [0.011 secs, 2652 KB]
  Test 6: TEST OK [0.011 secs, 2652 KB]
  Test 7: TEST OK [0.032 secs, 2652 KB]
  Test 8: TEST OK [0.378 secs, 2652 KB]
  Test 9: TEST OK [0.367 secs, 2652 KB]
  Test 10: TEST OK [0.346 secs, 2652 KB]
  Test 11: TEST OK [0.248 secs, 2652 KB]
  Test 12: TEST OK [0.054 secs, 2652 KB]
  Test 13: TEST OK [0.011 secs, 2652 KB]
  Test 14: TEST OK [0.097 secs, 2652 KB]

微慢了点。


[编辑] 后缀数组

此题可以使用后缀数组,但需要做稍微改动。为了避免类似“aaaa”这样的数据带来的问题,我们可以在串的末尾加上无限个值为无限的字符。当然,没有必要真的加。只需要将下面两句话交换即可:

   for(p=0,i=n-j+1;i<=n;++i)   y[++p]=i;
   for(i=1;i<=n;++i)   if(sa[i]>j) y[++p]=sa[i]-j;

   for(p=0,i=1;i<=n;++i)   if(sa[i]>j) y[++p]=sa[i]-j;
   for(i=n-j+1;i<=n;++i)   y[++p]=i;
  Test 1: TEST OK [0.000 secs, 7364 KB]
  Test 2: TEST OK [0.000 secs, 7364 KB]
  Test 3: TEST OK [0.000 secs, 7364 KB]
  Test 4: TEST OK [0.000 secs, 7364 KB]
  Test 5: TEST OK [0.000 secs, 7364 KB]
  Test 6: TEST OK [0.000 secs, 7364 KB]
  Test 7: TEST OK [0.027 secs, 7364 KB]
  Test 8: TEST OK [0.270 secs, 7364 KB]
  Test 9: TEST OK [0.243 secs, 7364 KB]
  Test 10: TEST OK [0.243 secs, 7364 KB]
  Test 11: TEST OK [0.108 secs, 7364 KB]
  Test 12: TEST OK [0.000 secs, 7364 KB]
  Test 13: TEST OK [0.000 secs, 7364 KB]
  Test 14: TEST OK [0.027 secs, 7364 KB]


ps: 很明显的最小表示法吗……建议参考WC周源《最小表示思想的应用》,简洁易懂,程序还很快……

简单的代码实现请参考Pascal范例~

Executing...

  Test 1: TEST OK [0.000 secs, 208 KB]
  Test 2: TEST OK [0.000 secs, 240 KB]
  Test 3: TEST OK [0.000 secs, 240 KB]
  Test 4: TEST OK [0.011 secs, 240 KB]
  Test 5: TEST OK [0.000 secs, 272 KB]
  Test 6: TEST OK [0.000 secs, 304 KB]
  Test 7: TEST OK [0.000 secs, 304 KB]
  Test 8: TEST OK [0.032 secs, 304 KB]
  Test 9: TEST OK [0.022 secs, 304 KB]
  Test 10: TEST OK [0.043 secs, 304 KB]
  Test 11: TEST OK [0.022 secs, 304 KB]
  Test 12: TEST OK [0.000 secs, 304 KB]
  Test 13: TEST OK [0.011 secs, 208 KB]
  Test 14: TEST OK [0.000 secs, 304 KB]

[编辑] 求助

请问最小表示法能保证正确性吗,有可能找出的是另一串相同的字符,而并非最小表示的啊.

(设指针i,j分别向后滑动k个位置后比较失败(k>=0)。假设s[i+k]≠s[j+k]:讨论 s[i+k] > s[j+k]:因为s[i+x]与s[j+x](0<=x<=k-1)对应相等,即有:s[i...i+k-1] = s[j...j+k-1],现在已经在i+k位置处与j+k不相等且s[i+k]>s[j+k],因此对指针i而言整个s[i]到s[k-1]都不是s的最小表示的起始位置,则PosMin(s)>i+k,所以i可以滑动到i+k+1仍可保证<=PosMin(s)。 反之对称地s[i+k]<s[j+k],j可以滑动到j+k+1仍可保证<=PosMin(s)。不管怎么样总有min(i,j) <= PosMin(s),当k==len时,返回min(i,j)即为答案。因为i,j都是单调递增的,复杂度O(n)。)

[编辑] ORZ

硬是没看懂为什么是O(n),但是我的时间很好的证明了:

USER: wang x [wx539181] TASK: hidden LANG: C++

Compiling... Compile: OK

Executing...

  Test 1: TEST OK [0.000 secs, 3328 KB]
  Test 2: TEST OK [0.000 secs, 3328 KB]
  Test 3: TEST OK [0.000 secs, 3328 KB]
  Test 4: TEST OK [0.000 secs, 3328 KB]
  Test 5: TEST OK [0.000 secs, 3328 KB]
  Test 6: TEST OK [0.000 secs, 3328 KB]
  Test 7: TEST OK [0.000 secs, 3328 KB]
  Test 8: TEST OK [0.000 secs, 3328 KB]
  Test 9: TEST OK [0.000 secs, 3328 KB]
  Test 10: TEST OK [0.000 secs, 3328 KB]
  Test 11: TEST OK [0.000 secs, 3328 KB]
  Test 12: TEST OK [0.000 secs, 3328 KB]
  Test 13: TEST OK [0.000 secs, 3328 KB]
  Test 14: TEST OK [0.000 secs, 3328 KB]

All tests OK.详见C++ CODE

[编辑] 直接暴力就好了

可以在读入时统计每个字母第几次出现在哪个位置, 然后按字典序找到第一个出现次数非零的字母. 之后倒序记录每个位置上的该字母的连续出现次数Count[i], 并更新最大连续出现次数MAX, 特别注意此题是环状结构. 之后只处理Count[i] == MAX的位即可.

Executing...

  Test 1: TEST OK [0.000 secs, 13964 KB]
  Test 2: TEST OK [0.027 secs, 13964 KB]
  Test 3: TEST OK [0.027 secs, 13964 KB]
  Test 4: TEST OK [0.027 secs, 13964 KB]
  Test 5: TEST OK [0.027 secs, 13964 KB]
  Test 6: TEST OK [0.027 secs, 13964 KB]
  Test 7: TEST OK [0.027 secs, 13964 KB]
  Test 8: TEST OK [0.027 secs, 14076 KB]
  Test 9: TEST OK [0.027 secs, 13964 KB]
  Test 10: TEST OK [0.027 secs, 13964 KB]
  Test 11: TEST OK [0.027 secs, 13964 KB]
  Test 12: TEST OK [0.027 secs, 13964 KB]
  Test 13: TEST OK [0.027 secs, 13964 KB]
  Test 14: TEST OK [0.918 secs, 13964 KB]

All tests OK.

最后一个点比较尴尬, 第11个点很明显是卡找A的..--Climber.pI


更猥琐的暴力—DY


  Test 1: TEST OK [0.000 secs, 664 KB]
  Test 2: TEST OK [0.000 secs, 664 KB]
  Test 3: TEST OK [0.000 secs, 664 KB]
  Test 4: TEST OK [0.000 secs, 664 KB]
  Test 5: TEST OK [0.000 secs, 664 KB]
  Test 6: TEST OK [0.000 secs, 664 KB]
  Test 7: TEST OK [0.000 secs, 664 KB]
  Test 8: TEST OK [0.054 secs, 664 KB]
  Test 9: TEST OK [0.248 secs, 664 KB]
  Test 10: TEST OK [0.378 secs, 664 KB]
  Test 11: TEST OK [0.000 secs, 664 KB]
  Test 12: TEST OK [0.000 secs, 664 KB]
  Test 13: TEST OK [0.000 secs, 664 KB]
  Test 14: TEST OK [0.367 secs, 664 KB]

All tests OK.

只要扫描一遍,把最小的字母位置记录下来。而且连续的一串只需记第一个(想一想,为什么)。//冒充一下LRJ
然后枚举比较即可。



实在是没时间,只好写暴力,各位注意一下c++的string在暴力的时候很不好用,它那个[]操作符是有各种额外check的

要是想过,就得来个 const char* p = str.c_str();,然后把p当作c字符串来搞。 -- dp


红书模板P216..注意读入时候可能换行 ```

   int pos = 0;
   while (pos < N ) {
       scanf("%s",str + pos);
       pos = (int)strlen(str);
   }

```

[编辑] 参考代码

C

C++

Pascal

个人工具