您的位置:首页 > 编程语言 > C语言/C++

Best Cow Line

2015-09-29 23:29 399 查看
标签: C++  最小字母串  排序 贪心 

起因:因为作业要求需要在http://poj.org/上解决题目,无聊又瞎做了一题:3617题Best Cow Line,想了一下写一个函数自我嵌套就解决了,看了网上别人的代码觉得好烦好乱,于是晒出来交流交流。

问题:大意是要求对一个字母串重新排列使得新串的字典序最小,比如原来的字母串S1=“ACDBCB”重排后为S2=“ABCBCD”。

题目中给了重排的方法:每次取S1的头或尾字母,择其小者添加到S2的后面,直到取完。

疑惑:

刚拿到题目马上就有几个问题需要解决了:

1.什么是字典序最小的字母串?

2.为什么这样的重排方法可以得到最小的字母串?

3.如何实现题目中给出的重排方法?

4.还有其他方法可以得到最小字母串吗?又如何实现?

思考:

1.最小字母串是对一个给定的字符集来说,它的所有排列中按字典序排最小的那些字母串。这里涉及到两个字符串的比较大小,从头比到尾,只要有一次比出了大小,后面就不用比了。也就是说如果s1<s2,当且仅当存在下标m,s1[m]<s2[m],并且s1[k]=s2[k](k<m)。

2.这是一个最优化问题,对给定的N个字母组成的字符串,要得到最小的字母串,最直接的就是全部一口气从小到大排序,冒泡、选择、擂台、二分、快速……这样得到必然是最小的字母串,时间复杂度在n^2到n*logn之间。也就是说题目中给出的方法得到的并不是真正的最小字母串,因为对于原字符串S1=“ACDBCB”来说,按我理解的最小字母串的定义,最小的应该是“ABBCCD”,而不是S2=“ABCBCD”。

那么为什么采用这种方法呢?它基于局部最优的思想,目光只局限在头尾,只要知道头尾哪个最小就认为拿到了最小的字母,开心地放到新的字母串去,这种贪图局部最优化的思想被叫作贪心思想,它放弃全局最优的宏图伟业,企图换来时间上的优势,同时也能得到相对较优的结果。另外,像是两头队列这种出入限制的情形。

3.既然这个思想如此形象:目光只需要关注S1的头尾,你只要告诉我S1的头尾哪个比较小,我就傻乎乎地取走哪个,一点不在乎它是不是全局最小的,那么我就用代码来模拟这个思想的过程。

既然我们需要关注S1的头尾,就定义两个下标int head=0,tail=N-1;表示S1
的头尾。

同时我们还需要有人来告诉我们当前S1的头尾是哪一个比较小,这个人就是一个函数compa,我们告诉他S1,head,tail,它就返回一个整数,如果是0,就说明head这个位置比较小,如果是1,就说明tail这个位置比较小。

由于S1有了,head和tail已经有了初值,我们马上可以去问那个人哪个比较小了?

int compa(char S1[],int head,int tail){

//如果s1[head]<s1[tail],告诉全世界是head这个位置比较小,返回0;如果大呢?就返回1

if(s1[head]<s1[tail]) return 0;

if(s1[head]>s1[tail]) return 1;

//关键就在于如果头尾相等怎么办呢?随便选一个吗?不行,因为贪心的想法还没满足呢,必须比出一个较小的来我们才罢休,既然这里问不出较小的是哪个,那就继续问下去,再问一遍函数compa,这就出现函数的自身嵌套了,马上一个警告给自己,函数嵌套别忘了明确终止条件,即什么时候嵌套结束,不能无限嵌套。很明显,最坏的情况是我们一路问下去,后面的头尾都是相等的,也就是出现tail-head<3,这就可以随便选一个了,我们就任性取头好了。

if(tail-head<3) return 0;    

return compa(S1[],head++,tail--);

}

给出完整代码吧,很简洁的20行代码,去掉输入字符串的语句和常规语句,核心的也就不到10行,相比那些大量使用跳转、判断、flag的代码更显精致和思路清晰。

完整代码

#include <iostream>

using namespace std;

int compa(char c[],int i,int j){
if(c[i]<c[j])return 0;
if(c[i]>c[j])return 1;
if(j-i<3)return 0;

  return compa(c,i+1,j-1);

}

int main(){
int N;cin>>N;
char* c;c=(char*)malloc(N*sizeof(char));
for(int i=0;i<N;i++)cin>>c[i];
int i=0,j=N-1,k=1;
while(j!=i){
if(compa(c,i,j))cout<<c[j--];
else cout<<c[i++];
if(!(k++%80)) cout<<endl; //题目要求隔80个换行输出
}
cout<<c[i];

return 0;

}

4.能得到全局最小字母串的算法肯定是排序算法了,那么还有其他局部最优的算法吗?最直接能想到的就是对这个贪心算法变形和改进。

我们首先来分析一下这个算法的性能。对N个字符的字母串,它的比较次数最坏是O(n^2),但这种情况是极端的,也就是全部相等,一个简单的衡量它时间复杂度的方法是计算N个字符中相同字符的比例,因为影响算法时间的就是因为相等造成的再比较。题目中字符只有A-Z这26个,那么相同的字符的期望是[2*C(2,N)+3*C(3,N)+……+N*C(N,N)]*26/26^(N)=(N*2^(N-1)-N)/26^(N-1)=O(0.077^(N-1))

也就是说,当N越大,它所需的时间成指数式的减少,这是相当可观的。

对于这个算法自然的变形就是不看头尾,从中间的两个数开始比较,不断往左右两边去比较;甚至按照某一规则从任意两个地方的数开始比较,按照这一规则不断选择下一对进行比较,比如开始时head=2,tail=N-3;每次比较后头总是往下走3位,尾巴总是往上走2位,直到相遇或走到边界。当然走到边界时你可以停止走下去,也可以设置为继续循环走回来,但是这时候要注意规则设置时不要出现头尾永远不能相遇的情形,即头尾走的步数应该是互质的。我们还可以继续变形,每次比较不仅仅只比两个数,而是比较三个数,四个数甚至更多,也就是进一步把局部比较的范围扩大,你会发现当你把范围扩大最大N个的时候,就是全局最优了,算法也就进化为排序算法了,而你从什么地方开始选择head、tail.....这些下标以及设置下标移动的规则,就会演变为不同类型的排序算法。

当然这个算法还可以退化,就是只看一个,也就是什么都不比,每次随便拿来一个字母就好,这个的时间复杂度显然最低O(N)。

总结:

对于N个符号的一维数组,一旦给定了数和数组间的某种序规则和最小序的概念,就有求最小序的需求和排序算法。从这题的思考中我们似乎看到了求最小序算法的进化历程,从不排序到各种局部排序再到全排序。其中影响算法时间复杂度的关键就在于下标的选择和移动规则,这说明合理的选择比较的位置和移动规则可以减少排序时间。注意到当我们选择局部排序时,也同样涉及到如何从局部字符中找出最小的字符的问题,相当于局部的全排序。

我脑子中排序的概念是知道了有限数组每个元素的序关系,如何建立元素和整数0~N-1的一一对应关系,这样拿到一个字符就直接把它放到对应的位置就好。这和给出的序关系的形式有关,如果给出的序关系的形式就是一个和已知序关系的一一映射,那么问题已经解决;如果所有元素的序关系是通过两两元素的序关系给出的,这就是经典的排序算法中通过两两比较来解决问题。

那么对于一个只有A~Z的字符串S1
,事实上已经建立了整数x:0~25的对应关系,也就是x=S1[i]-'A'

只要有newc[26]
,ind
={0},就可以直接放进来newc[S1[i]-'A'][ind[S1[i]-'A']++]=S1[i]

而事实上计算机所进行的排序问题,几乎都是和已知的序关系建立了一一映射的,如果能明确的表达出这层关系就可以直接放进来,而能否明确表达出这一映射关系,取决于是否事先知道数组中的上下界。关键信息越多,所能解决问题的效率当然越高。另外如果没有足够的空间,仅仅限制在N个位置的空间上,这就是那些经典的排序算法了,它需要考虑如何挪空间。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ poj 排序 贪心