Category Archives: hihoCoder

hihoCoder 1062-最近公共祖先·一

hihoCoder 1062-最近公共祖先·一 #1062 : 最近公共祖先·一 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho最近发现了一个神奇的网站!虽然还不够像58同城那样神奇,但这个网站仍然让小Ho乐在其中,但这是为什么呢? “为什么呢?”小Hi如是问道,在他的观察中小Ho已经沉迷这个网站一周之久了,甚至连他心爱的树玩具都弃置一边。 “嘿嘿,小Hi,你快过来看!”小Ho招呼道。 “你看,在这个对话框里输入我的名字,在另一个对话框里,输入你的名字,再点这个查询按钮,就可以查出来……什么!我们居然有同一个祖祖祖祖祖爷爷?” “诶,真是诶……这个网站有点厉害啊。”小Hi不由感叹道。 “是啊,这是什么算法啊,这么厉害!”小Ho也附和道。 “别2,我说的是他能弄到这些数据很厉害,而人类的繁殖树这种层数比较浅的树对这类算法的要求可是简单的不得了,你都能写出来呢!”小Hi道。 “啊?我也能写出来?可是……该从哪开始呢?”小Ho困惑了。 小Ho要面临的问题是这样的,假设现在他知道了N个人的信息——他们的父亲是谁,他需要对于小Hi的每一次提问——两个人的名字,告诉小Hi这两个人的是否存在同一个祖先,如果存在,那么他们的所有共同祖先中辈分最低的一个是谁? 提示:不着急,慢慢来,另外我有一个问题:挖掘机技术哪家强?! 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。 每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。 每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。 对于100%的数据,满足N<=10^2,M<=10^2, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人)。 输出 对于每组测试数据,对于每个小Hi的询问,输出一行,表示查询的结果:如果根据已知信息,可以判定询问中的两个人存在共同的祖先,则输出他们的所有共同祖先中辈分最低的一个人的名字,否则输出-1。 样例输入 11 JiaYan JiaDaihua JiaDaihua JiaFu JiaDaihua JiaJing JiaJing JiaZhen JiaZhen JiaRong JiaYuan JiaDaishan JiaDaishan JiaShe JiaDaishan JiaZheng JiaShe JiaLian JiaZheng JiaZhu JiaZheng JiaBaoyu 3 JiaBaoyu JiaLian JiaBaoyu JiaZheng JiaBaoyu LinDaiyu 样例输出 JiaDaishan JiaZheng -1


最近公共祖先问题:给定一个家谱图,问某两个人的最近的公共祖先是谁,如果没有公共祖先,输出-1。最简单的方法题目提示得很清楚了:
当然你也先将一个人的祖先全都标记出来,然后顺着另一个的父亲一直向上找,直到找到第一个被标记过的结点,便是它们的最近公共祖先结点了。
所以我就简单粗暴的使用STL的MAP和SET来解决问题了。假设需要判断的两个人名分别为son1和son2,那么①首先把*son1*及其所有祖先都找到并加入到一个set中,然后②首先判断son2在不在前面的set中,③如果不在,再依次判断son2的祖先在不在set中。 这里需要注意几点: 1. son1的祖先包括son1本身,比如第二个样例中的JiaBaoyu和JiaZheng,JiaZheng的祖先就包括JiaZheng自己,所以需要上面的第①和②步。 2. 为什么使用set而不是vector呢?因为②③步需要在son1的祖先中搜索,set内部用红黑树实现,搜索的效率高于vector。 知道了以上几点,就可以很快的写出代码了,完整代码如下: [cpp] #include<iostream> #include<map> #include<string> #include <set> using namespace std; int main() { //freopen("input.txt","r",stdin); int n,t; cin>>n; string son,father; map<string,string> son_father;//map[son]=father for(int i=0;i<n;i++) { cin>>father>>son; son_father.insert(make_pair(son,father)); } cin>>t; string son1,son2,comm_father; while(t–) { comm_father=""; cin>>son1>>son2; //vector<string> grand_fathers; set<string> grand_fathers;//使用set的查找效率高于vector grand_fathers.insert(son1);//注意,son1,son2本身也是一个祖先,一定要加进去,否则错! while(son_father.find(son1)!=son_father.end()) { grand_fathers.insert(son_father[son1]); son1=son_father[son1]; } if(grand_fathers.find(son2)!=grand_fathers.end())//首先son2本身也是一个祖先,所以先判断一下//使用set的查找效率高于vector { if(t!=0) cout<<son2<<endl; else cout<<son2; continue; } while(son_father.find(son2)!=son_father.end()) { if(grand_fathers.find(son_father[son2])!=grand_fathers.end()) { comm_father=son_father[son2];//因为是son2从下往上找,所以第一个出下的肯定是最近的公共祖先 break; } else son2=son_father[son2]; } if(t!=0) { if(comm_father=="") cout<<"-1"<<endl; else cout<<comm_father<<endl; } else { if(comm_father=="") cout<<"-1"; else cout<<comm_father; } } return 0; } [/cpp] 本代码提交AC,用时4MS,内存0MB。 之前说了,我这种解法是非常简单粗暴的,事实上最近公共祖先问题是一个经典问题,有很多经典算法可以解决,我后面会进一步研究。]]>

hihoCoder 1052-基因工程

hihoCoder 1052-基因工程 #1052 : 基因工程 时间限制:1000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho正在进行一项基因工程实验。他们要修改一段长度为N的DNA序列,使得这段DNA上最前面的K个碱基组成的序列与最后面的K个碱基组成的序列完全一致。 例如对于序列”ATCGATAC”和K=2,可以通过将第二个碱基修改为”C”使得最前面2个碱基与最后面两个碱基都为”AC”。当然还存在其他修改方法,例如将最后一个碱基改为”T”,或者直接将最前面两个和最后面两个碱基都修改为”GG”。 小Hi和小Ho希望知道在所有方法中,修改碱基最少的方法需要修改多少个碱基。 输入 第一行包含一个整数T(1 <= T <= 10),代表测试数据的数量。 每组测试数据包含2行,第一行是一个由”ATCG”4个大写字母组成的长度为N(1 <= N <= 1000)的字符串。第二行是一个整数K(1 <= K <= N)。 输出 对于每组数据输出最少需要修改的碱基数量。 样例输入 2 ATCGATAC 2 ATACGTCT 6 样例输出 1 3


简单的字符串处理题目,该题要求修改字符保证前k个字符和后k个字符相同,求最小的修改次数。这个题目会让人想到字符串的编辑距离,但不完全相同,因为这题是对单个字符串的修改,不能想当然的把前后k个字符取出来,然后逐一比较是否相等,因为如果前k个字符和后k个字符有重叠的话,你修改了某个字符,会影响下一次比较。 比如字符串CTACATGT,长度n=8,k=6,前k个字符串为s1=CTACAT,后k个字符串为s2=ACATGT,我们不能单独求s1和s2的距离,而是需要把这些字符放到原字符串中,再进一步研究你会发现其实就是在原字符串中保证s[i]=s[i+2],其中2=d=n-k。也就是下图中红色和绿色的两组字符要分别相同。 现在我们分析红色那组字符,首先s[0]==s[2]不成立,可以把s[0]改成A;也可以把s[2]改成C,如果我们把s[2]改成C的话,虽然s[0]==s[2]成立,但是s[2]==s[4]又不成立了,又要修改s[4];但是如果我们把s[0]改成A的话,则s[0]==s[2]==s[4]都成立,且修改次数更少。那么要怎样才能使修改的次数最少呢? 我们回到问题的本源:使用红色那组字符都相同,为了使修改次数最少,我们当然是将所有字符统一成当前出现次数最多的那个字符咯,这样只需要修改较少的非同一字符,在这个例子中,就是修改成出现次数最多的A。 如果前k个字符串和后k个字符串没有重叠,那就很简单了,只需依次检查对应字符是否相等。 理解了以上内容,代码很快就能写出来了: [cpp] #include<iostream> #include<string> using namespace std; //求4个数的最大值 int max4(int a,int b,int c,int d) { int maxab=a>b?a:b; int maxcd=c>d?c:d; return maxab>maxcd?maxab:maxcd; } int main() { int t,k,n,d,ans;//t,k为题意;n为字符串长度;d为n-k相隔为d的字符需要相等;ans表示需要修改的碱基数量。 string s; cin>>t; while(t–) { ans=0; cin>>s>>k; n=s.size(); d=n-k; if(2*k<=n)//如果最前k个碱基和最后k个碱基没有重叠的话 { for(int i=0;i<k;i++) if(s[i]!=s[i+d])//只需要依次检查对应字符是否相等即可 ans++; } else//如果有重叠,则另行讨论 { for(int i=0;i<d;i++)//把所有碱基分成d组,需要确保每组都是同一个字符 { int A=0,T=0,G=0,C=0,num=0; for(int j=i;j<n;j+=d)//对于每一组,求出该组中出现次数最多的碱基的个数 { num++; if(s[j]==’A’) A++; else if(s[j]==’T’) T++; else if(s[j]==’G’) G++; else C++; } int maxATGC=max4(A,T,G,C);//该组全部修改成出现次数最多的碱基 int change_num=num-maxATGC;//用该组中所有碱基个数减去出现次数最多的碱基个数,就是需要修改的碱基个数 ans+=change_num; } } cout<<ans<<endl; } return 0; } [/cpp] 本代码提交AC,用时1MS,内存0MB。 ]]>

hihoCoder 1051-补提交卡

hihoCoder 1051-补提交卡 #1051 : 补提交卡 时间限制:2000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho给自己定了一个宏伟的目标:连续100天每天坚持在hihoCoder上提交一个程序。100天过去了,小Ho查看自己的提交记录发现有N天因为贪玩忘记提交了。于是小Ho软磨硬泡、强忍着小Hi鄙视的眼神从小Hi那里要来M张”补提交卡”。每张”补提交卡”都可以补回一天的提交,将原本没有提交程序的一天变成有提交程序的一天。小Ho想知道通过利用这M张补提交卡,可以使自己的”最长连续提交天数”最多变成多少天。 输入 第一行是一个整数T(1 <= T <= 10),代表测试数据的组数。 每个测试数据第一行是2个整数N和M(0 <= N, M <= 100)。第二行包含N个整数a1, a2, … aN(1 <= a1 < a2 < … < aN <= 100),表示第a1, a2, … aN天小Ho没有提交程序。 输出 对于每组数据,输出通过使用补提交卡小Ho的最长连续提交天数最多变成多少。 样例输入 3 5 1 34 77 82 83 84 5 2 10 30 55 56 90 5 10 10 30 55 56 90 样例输出 76 59 100


这一题想到那个点上就不难了,题目要求将补提交卡插入空缺的某天中,使连续提交天数最长。要使连续提交天数最长,补提交卡插入的位置也一定要是连续的,而且很方便的是,题目中给出的空缺天数都是递增的,所以不需要重新排序了。 既然需要满足插入的位置也要连续,那么总的方案数就没有那么多了。比如n=5,m=2,有如下几种方案: 左边的红色数字表示不同方案标号,这个标号也正好是该方案中数组a中的第一个插空位置的下标。很容易得到总的方案数有n-m+1个,所以我们只需将i从0到n-m+1循环枚举每一种方案,求最长的连续提交天数。 对于某一种方案,如上所述,其插空的开始下标正好为i,因为要插m个空,所以结束下标为m+i-1。那么这两者之间总的连续提交天数怎么算呢?比如上图中的方案2,如果把第55和56天填上,那么实际的连续提交天数应该是从31~89.也就是要从i-1对应的那天的后一天(31)算起,到m+i-1-1对应的那天的前一天(89)结束,总共就是89-31+1,也就是a[m+i-1+1]-a[i-1]-1;如果遇到最后一个空,因为m+i-1+1可能超过数组a的范围,所以我们添加一个a[n]=101,俗称哨兵;如果i==0的话,没有i-1,所以分开讨论。 最终的代码如下: [cpp] #include<iostream> #include<vector> using namespace std; int main() { int t; cin>>t; int n,m; while(t–) { cin>>n>>m; vector<int> a(n+1); for(int i=0;i<n;i++) cin>>a[i]; a[n]=101;//哨兵 if(m>=n) cout<<100<<endl; else { int max_days=0;//最长连续提交天数 int case_num=n-m+1;//总的枚举情况数 for(int i=0;i<case_num;i++) { int last_index=m+i-1;//该方案下最后一个下标 if(i==0) { if(a[last_index+1]-1>max_days) max_days=a[last_index+1]-1; } else { if(a[last_index+1]-a[i-1]-1>max_days) max_days=a[last_index+1]-a[i-1]-1; } } cout<<max_days<<endl; } } return 0; } [/cpp] 本代码提交AC,用时0MS,内存0MB。 ]]>

hihoCoder 1043-完全背包

hihoCoder 1043-完全背包 #1043 : 完全背包 时间限制:20000ms 单点时限:1000ms 内存限制:256MB 描述 且说之前的故事里,小Hi和小Ho费劲心思终于拿到了茫茫多的奖券!而现在,终于到了小Ho领取奖励的时刻了! 等等,这段故事为何似曾相识?这就要从平行宇宙理论说起了………总而言之,在另一个宇宙中,小Ho面临的问题发生了细微的变化! 小Ho现在手上有M张奖券,而奖品区有N种奖品,分别标号为1到N,其中第i种奖品需要need(i)张奖券进行兑换,并且可以兑换无数次,为了使得辛苦得到的奖券不白白浪费,小Ho给每件奖品都评了分,其中第i件奖品的评分值为value(i),表示他对这件奖品的喜好值。现在他想知道,凭借他手上的这些奖券,可以换到哪些奖品,使得这些奖品的喜好值之和能够最大。 提示一: 切,不就是0~1变成了0~K么 提示二:强迫症患者总是会将状态转移方程优化一遍又一遍 提示三:同样不要忘了优化空间哦! 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为两个正整数N和M,表示奖品的种数,以及小Ho手中的奖券数。 接下来的n行描述每一行描述一种奖品,其中第i行为两个整数need(i)和value(i),意义如前文所述。 测试数据保证 对于100%的数据,N的值不超过500,M的值不超过10^5 对于100%的数据,need(i)不超过2*10^5, value(i)不超过10^3 输出 对于每组测试数据,输出一个整数Ans,表示小Ho可以获得的总喜好值。 样例输入 5 1000 144 990 487 436 210 673 567 58 1056 897 样例输出 5940


这一题和之前的0-1背包很类似,只需要稍微修改一下就可以了。 我们先来复习一下之前的0-1背包。0-1背包对于一件物品,要么兑换,要么不兑换,只有两种可能。假设V[i,j]表示用j张奖券兑换前i个物品,则对于第i个物品,我们只有两种选择:1.兑换;2.不兑换。如果不兑换,则V[i,j]=V[i-1,j],也就是不要第i件物品,用j张奖券兑换前i-1个物品;如果兑换,则V[i,j]=V[i-1,j-need[i]]+value[i],也就是一定要第i件物品,所以+value[i],然后再看兑换完第i件物品后剩余的奖券兑换前i-1个物品能得到多少价值。最后就是求这两种情况的最大值。用数学公式表示就是: $$!V[i,j] = \begin{cases}0 & \text{if i=0 or j=0} \\V[i-1,j] & \text{if j < need[i]} \\max \{ V[i-1,j],V[i-1,j-need[i]]+value[i] \} & \text{if i>0 and j$\geq$ need[i]}\\\end{cases}$$ 上面是0-1背包的动态规划状态转换方程,详细的代码可以参考上面的链接。对于完全背包,只需稍微修改即可。 我们首先要理解完全背包的含义,完全背包不限制物品的个数,也就是只要奖券够多,可以兑换多个同一种物品但是对于某一个物品,也只有两种情况,要么兑换,要么不兑换,不可以将某一件物品拆分成小部分,只兑换其中的0.x。这个地方容易和分数背包混淆。分数背包和0-1类似,每一种物品也只有一个,你可以完整的兑换这一个物品,也可以只兑换这个物品的一部分,但是一种物品只有一个。分数背包可以用贪心算法快速求解,可以参考《算法导论第三版》中文版P243关于0-1背包和分数背包的讨论。 理解了完全背包的含义,我们再来推导其状态转换方程。这个时候对于第i个物品,我们有多种选择:1.不兑换;2.兑换1个;3.多换多个。同样的,我们要从中选一个能使价值最大的方案。如果不兑换,和0-1背包一样,有V[i,j]=V[i-1,j];但是如果兑换,是不是得一一尝试兑换1个、2个…的所有方案呢?可以这样做,但是还有一个更巧妙的方法,我们先给出公式再解释: $$!V[i,j] = \begin{cases}0 & \text{if i=0 or j=0} \\V[i-1,j] & \text{if j0 and j$\geq$ need[i]}\\\end{cases}$$ 大家仔细对比0-1背包和完全背包的状态转换方程,只改动了一个微小的地方。对于第i个物品,我们如果兑换,则肯定要+value[i],这是剩下j-need[i]张奖券,因为第i个物品可以兑换多个,所以我们还是用j-need[i]张奖券兑换前i个物品,而不是只兑换前i-1个物品,即V[i,j]=V[i,j-need[i]]+value[i]这样的话,程序自然还会考虑选择第i个物品,也就是兑换多个第i个物品。这就是0-1背包和完全背包的重要区别。 理解了这一点,我们很快就可以写出程序了。代码如下: [cpp] #include <iostream> #include<vector> using namespace std; int main() { int n,m; cin>>n>>m; vector<int> need_vi(n),value_vi(n); for(int i=0;i<n;i++) cin>>need_vi[i]>>value_vi[i]; vector<int> V(m+1,0);//V[j]表示有j张奖券,装前i个奖品获得的最大评分 for(int i=0;i<n;i++)//V[j]表示有j张奖券,装前i个奖品获得的最大评分,每个奖品可以装多个 { for(int j=need_vi[i];j<=m;j++)//从前往后填表,因为这是完全背包 { V[j]=(V[j-need_vi[i]]+value_vi[i]>V[j])?(V[j-need_vi[i]]+value_vi[i]):V[j]; } } cout<<V[m]; return 0; } [/cpp] 具体到写代码时,因为我们正是要利用兑换第i个物品时j=j-need[i]时的值,所以我们从左往右填(从前往后填),这样后面就能利用前面已经修改过的值了。大家可以自己画个表格填一下就知道了,同时请参考联系我之前关于0-1背包的博客。 本代码提交AC,用时148MS,内存0MB。]]>

hihoCoder 1040-矩形判断

hihoCoder 1040-矩形判断 #1040 : 矩形判断 时间限制:1000ms 单点时限:1000ms 内存限制:256MB 描述 给出平面上4条线段,判断这4条线段是否恰好围成一个面积大于0的矩形。 输入 输入第一行是一个整数T(1<=T<=100),代表测试数据的数量。 每组数据包含4行,每行包含4个整数x1, y1, x2, y2 (0 <= x1, y1, x2, y2 <= 100000);其中(x1, y1), (x2,y2)代表一条线段的两个端点。 输出 每组数据输出一行YES或者NO,表示输入的4条线段是否恰好围成矩形。 样例输入 3 0 0 0 1 1 0 1 1 0 1 1 1 1 0 0 0 0 1 2 3 1 0 3 2 3 2 2 3 1 0 0 1 0 1 1 0 1 0 2 0 2 0 1 1 1 1 0 1 样例输出 YES YES NO


这一题题意很简单,但是要写代码实现还是有很多细节需要考虑。 题目给出了4条线段的8个端点,问是否能由这4条线段构成一个面积大于0的矩形。脑海中立马浮现出一个矩形的形状。要构成一个矩形的首要条件是: 1. 有且只有4个互异的顶点 这个条件是显而易见的,如下图(1)给出的4条线段,虽然延长一下能组成矩形,但是很明显如果只由线段组成的话,不是一个矩形。 要判断给出的点是否只有4个点,第一反应是用set集合,只要把题目给出的8个点都加入到set中,判断集合大小是否是4就可以了。 首先给出点的定义: [cpp] //点结构体 typedef struct P { int x,y;//两点坐标 bool operator<(const P& p)const//如果要加入到set中,需要重载< { //return this->x<p.x&&this->y<p.y;//注意这样会导致(0,0)<(0,1)且(0,1)<(0,0)的情况 return (this->x<p.x)||((this->x==p.x)&&(this->y<p.y)); } bool operator==(const P&p)const//重载等于比较 { return (this->x==p.x)&&(this->y==p.y); } }; [/cpp] 需要注意一点,因为需要把P加入到set中,而set是通过红黑树来排序的,所以需要重载小于<操作符。我最开始重载函数是这样写的 [cpp] return this->x<p.x&&this->y<p.y; [/cpp] 但是这样会有问题,如果给出两个点(0,0)和(0,1),会得出(0,0)<(0,1)和(0,1)<(0,0)都不成立的结论,也就是说无法给(0,0)和(0,1)排序,也就无法插入到set中,所以需要修改成 [cpp] return (this->x<p.x)||((this->x==p.x)&&(this->y<p.y)); [/cpp] 这样就能判断(0,0)<(0,1)成立了。有关这个问题可以参考这个这个。 经过以上的步骤,就把问题转换成已知4个点,问能否由这4个点构成一个矩形的问题了。 此时又可以得出以下两个结论 2. 矩形中某条边和另外3条边的关系是:和其中2条边垂直,和另外1条边平行 3. 且和它平行的那条边不能是重合的边 有的同学可能会说,像上图中的(2)和(3)也是边a和另外2条边垂直、1条边平行啊,但是他们都不是矩形,但是大家别忘了,当我们走到这一步的时候,已经经过了第1个条件的检验,也就是只有4个顶点,你看图(2)和(3)明显不止4个顶点啊。所以结论2成立。 又有同学说,为什么还要结论3呢?因为还可能出现如上图(4)的样子,虽然图中所有边都满足条件2:和2条边垂直、和1条边平行,但是这明显不是一个矩形。因为边a和b重合了。所以结论3还是有必要的。 有了以上3条结论,我们就可以判断给出的4条线段是否能组成一个矩形了。 编程中有一些数学知识需要掌握的是,已知两个线段的4个端点,怎样判断这两条线段的关系:平行?垂直?其他? 很自然想到用斜率,如果k1*k2==-1则垂直,如果k1==k2则平行,否则其他。但是会遇到线段和y轴平行的情况,此时斜率是不存在的,所以需要稍微变一下。 假设线段L1的两个端点分别为(x1,y1)和(x2,y2),线段L2的两个端点分别为(a1,b1)和(a2,b2),则k1*k2==-1可以转换为(y2-y1)*(b2-b1)==-(x2-x1)*(a2-a1)。类似的,k1==k2转换为(y2-y1)*(a2-a1)==(b2-b1)*(x2-x1)。这样就不用担心斜率不存在的问题。 经过以上详细的分析,我们可以写出如下代码: [cpp] #include<iostream> #include<set> #include <vector> using namespace std; //点结构体 typedef struct P { int x,y;//两点坐标 bool operator<(const P& p)const//如果要加入到set中,需要重载< { //return this->x<p.x&&this->y<p.y;//注意这样会导致(0,0)<(0,1)且(0,1)<(0,0)的情况 return (this->x<p.x)||((this->x==p.x)&&(this->y<p.y)); } bool operator==(const P&p)const//重载等于比较 { return (this->x==p.x)&&(this->y==p.y); } }; //线结构体 typedef struct L { P p1,p2;//一条线由两点组成 }; //判断两条线的关系,1:垂直 0:平行 -1:其他 int check_two_line(L l1,L l2) { int x1=l1.p1.x,y1=l1.p1.y,x2=l1.p2.x,y2=l1.p2.y; int a1=l2.p1.x,b1=l2.p1.y,a2=l2.p2.x,b2=l2.p2.y; if((y2-y1)*(b2-b1)==-(x2-x1)*(a2-a1))//斜率公式的变形 return 1; else if((y2-y1)*(a2-a1)==(b2-b1)*(x2-x1)) return 0; else return -1; } //判断是否是同一条直线 bool is_same_line(L l1,L l2) { if(((l1.p1==l2.p1)&&(l1.p2==l2.p2))||((l1.p1==l2.p2)&&(l1.p2==l2.p1)))//如果线的两个端点都相同,则是同一条直线 return true; else return false; } //判断能否组成一个矩形 bool is_rect(vector<L> lines) { int vertical_num,parallel_num; for(int i=0;i<4;i++)//每条线都和剩余3条线判断关系 { vertical_num=0; parallel_num=0; for(int j=0;j<4;j++) { if(j==i)//不和自己判断 continue; int rs=check_two_line(lines[i],lines[j]); if(rs==-1) return false; else if(rs==1) vertical_num++; else { if(is_same_line(lines[i],lines[j]))//如果是平行线还要判断是否是相同的线 return false; parallel_num++; } } if(!(vertical_num==2&&parallel_num==1))//只有当该线和其他2条线垂直,1条线平行时,才能组成矩形 return false; } return true; } int main() { int T; cin>>T; while(T–) { set<P> points; vector<L> lines; for(int i=0;i<4;i++) { P p1,p2; cin>>p1.x>>p1.y>>p2.x>>p2.y; points.insert(p1); points.insert(p2); L l; l.p1=p1; l.p2=p2; lines.push_back(l); } if(points.size()!=4)//能组成矩形的必要条件是有且只有4个点,所以用set来判断更方便 cout<<"NO"<<endl; else { if(is_rect(lines)) cout<<"YES"<<endl; else cout<<"NO"<<endl; } } return 0; } [/cpp] 代码提交后AC,用时1MS,内存0MB,还不错。 ]]>

hihoCoder 1038-01背包

hihoCoder 1038-01背包 #1038 : 01背包 时间限制:20000ms 单点时限:1000ms 内存限制:256MB 描述 且说上一周的故事里,小Hi和小Ho费劲心思终于拿到了茫茫多的奖券!而现在,终于到了小Ho领取奖励的时刻了! 小Ho现在手上有M张奖券,而奖品区有N件奖品,分别标号为1到N,其中第i件奖品需要need(i)张奖券进行兑换,同时也只能兑换一次,为了使得辛苦得到的奖券不白白浪费,小Ho给每件奖品都评了分,其中第i件奖品的评分值为value(i),表示他对这件奖品的喜好值。现在他想知道,凭借他手上的这些奖券,可以换到哪些奖品,使得这些奖品的喜好值之和能够最大。 提示一:合理抽象问题、定义状态是动态规划最关键的一步 提示二:说过了减少时间消耗,我们再来看看如何减少空间消耗 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为两个正整数N和M,表示奖品的个数,以及小Ho手中的奖券数。 接下来的n行描述每一行描述一个奖品,其中第i行为两个整数need(i)和value(i),意义如前文所述。 测试数据保证 对于100%的数据,N的值不超过500,M的值不超过10^5 对于100%的数据,need(i)不超过2*10^5, value(i)不超过10^3 输出 对于每组测试数据,输出一个整数Ans,表示小Ho可以获得的总喜好值。 样例输入 5 1000 144 990 487 436 210 673 567 58 1056 897 样例输出 2099


很简单的0-1背包问题。重新复习一下算法课本P140就好。注意填表的时候,从右往左填,这样只需要一行的空间。如果从左往右填只用一行空间的话,新的数据会覆盖老的数据,但是后面填表的过程中要用到老的数据,所以不行。代码如下: [cpp] #include<iostream> #include<vector> using namespace std; int main() { int n,m; cin>>n>>m; vector<int> need_vi(n);//所需奖券 vector<int> value_vi(n);//评分值 for(int i=0;i<n;i++) cin>>need_vi[i]>>value_vi[i]; vector<int> V(m+1,0);//V[j]表示有j张奖券,装前i个奖品获得的最大评分 for(int i=0;i<n;i++)//V[j]表示有j张奖券,装前i个奖品获得的最大评分 { for(int j=m;j>=need_vi[i];j–)//从后往前填表,能减少空间 { V[j]=(V[j-need_vi[i]]+value_vi[i]>V[j])?(V[j-need_vi[i]]+value_vi[i]):V[j];//要这件奖品和不要的对比 } } cout<<V[m]; return 0; } [/cpp] 代码提交AC,用时148MS,内存0MB。]]>

hihoCoder 1039-字符消除

hihoCoder 1039-字符消除 #1039 : 字符消除 时间限制:1000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi最近在玩一个字符消除游戏。给定一个只包含大写字母”ABC”的字符串s,消除过程是如下进行的: 1)如果s包含长度超过1的由相同字母组成的子串,那么这些子串会被同时消除,余下的子串拼成新的字符串。例如”ABCCBCCCAA”中”CC”,”CCC”和”AA”会被同时消除,余下”AB”和”B”拼成新的字符串”ABB”。 2)上述消除会反复一轮一轮进行,直到新的字符串不包含相邻的相同字符为止。例如”ABCCBCCCAA”经过一轮消除得到”ABB”,再经过一轮消除得到”A” 游戏中的每一关小Hi都会面对一个字符串s。在消除开始前小Hi有机会在s中任意位置(第一个字符之前、最后一个字符之后以及相邻两个字符之间)插入任意一个字符(‘A’,’B’或者’C’),得到字符串t。t经过一系列消除后,小Hi的得分是消除掉的字符的总数。 请帮助小Hi计算要如何插入字符,才能获得最高得分。 输入 输入第一行是一个整数T(1<=T<=100),代表测试数据的数量。 之后T行每行一个由’A”B”C’组成的字符串s,长度不超过100。 输出 对于每一行输入的字符串,输出小Hi最高能得到的分数。 提示 第一组数据:在”ABCBCCCAA”的第2个字符后插入’C’得到”ABCCBCCCAA”,消除后得到”A”,总共消除9个字符(包括插入的’C’)。 第二组数据:”AAA”插入’A’得到”AAAA”,消除后得到””,总共消除4个字符。 第三组数据:无论是插入字符后得到”AABC”,”ABBC”还是”ABCC”都最多消除2个字符。 样例输入 3 ABCBCCCAA AAA ABC 样例输出 9 4 2


这个题目要稍微思考一下,当然很容易想到穷举测试,但我在边写代码的时候还是在担心穷举会不会超时什么的。后来结果还不错。 总体思路就是在字符串的所有可以插空的位置分别插入A、B、C看能消除多少个字符,然后求最大值。所以这里关键就是怎样求一个字符串最终能消除多少个字符。 题目给出的消除策略是这样的:
1)如果s包含长度超过1的由相同字母组成的子串,那么这些子串会被同时消除,余下的子串拼成新的字符串。 2)上述消除会反复一轮一轮进行,直到新的字符串不包含相邻的相同字符为止。
一定要注意,相同的子串同时消除。我一开始想用堆栈来做,来一个字符,判断是否和栈顶相同,如果相同,则消除,并统计消除个数。但是这样会有问题,比如字符串ABCCBCCCAA,如果用堆栈来做的话:进A,进B,进C,C,消除C,C,进B,消除B,B,进C,C,C,消除C,C,C,进A,A,消除A,A,A,这样所有字符都消除了。但是按原题的思路,所有相同的子串同时消除,第一轮同时只能消除C,C,C,C,C,A,A,得到ABB,然后消除B,B,最终剩A。所以用堆栈不行。 后来是这样解决的,只需遍历一遍字符串,如果s[i]==s[i+1]则说明出现重复子串了,令j=i+1,然后一个while(s[i]==s[j])循环计数。如果可以消除,则递归调用该函数继续消除,否则直接返回。 代码如下: [cpp] #include <iostream> #include<string> using namespace std; //递归求解一个字符串最多能消除多少个字符 int get_erase_num(string s,int rs) { int G[100]={0};//用来记录整个字符串能够消除的字符的位置 int s_size=s.size(); bool is_done=true;//是否检查结束的标识 for(int i=0;i<s_size-1;i++)//注意上限s_size-1 { if(s[i]==s[i+1])//然后判断是否有至少两个连续的字符出现 { G[i]=1; is_done=false; rs++; int j=i+1; while(s[i]==s[j]) { G[j]=1; rs++; j++; if(j>=s_size)//检查是否超限 { break; } } i=j-1; } } if(!is_done)//如果可以消除,则还没有结束 { string ss=""; for(int i=0;i<s_size;i++) { if(G[i]==0) ss+=s[i];//同时消除重复子串后的结果串 } return get_erase_num(ss,rs);//递归 } return rs;//结束返回 } int main() { int n; string s; cin>>n; while(n–) { cin>>s; int s_size=s.size(); int max_m=-1; //在所有可以插入的位置依次分别测试插入A,B,C的结果 for(int i=0;i<s_size+1;i++) { if(i==s_size)//如果要在字符串的末尾插入字符 { int A_result=get_erase_num(s+"A",0); if(A_result>max_m) max_m=A_result; int B_result=get_erase_num(s+"B",0); if(B_result>max_m) max_m=B_result; int C_result=get_erase_num(s+"C",0); if(C_result>max_m) max_m=C_result; } else//插入位置除了末尾 { int A_result=get_erase_num(s.substr(0,i)+"A"+s.substr(i),0); if(A_result>max_m) max_m=A_result; int B_result=get_erase_num(s.substr(0,i)+"B"+s.substr(i),0); if(B_result>max_m) max_m=B_result; int C_result=get_erase_num(s.substr(0,i)+"C"+s.substr(i),0); if(C_result>max_m) max_m=C_result; } } cout<<max_m<<endl; } return 0; } [/cpp] 该代码提交结果AC,运行用时45ms,内存0MB,还不错。]]>

hihoCoder 1032-最长回文子串

hihoCoder 1032-最长回文子串 #1032 : 最长回文子串 时间限制:1000ms 单点时限:1000ms 内存限制:64MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进。 这一天,他们遇到了一连串的字符串,于是小Hi就向小Ho提出了那个经典的问题:“小Ho,你能不能分别在这些字符串中找到它们每一个的最长回文子串呢?” 小Ho奇怪的问道:“什么叫做最长回文子串呢?” 小Hi回答道:“一个字符串中连续的一段就是这个字符串的子串,而回文串指的是12421这种从前往后读和从后往前读一模一样的字符串,所以最长回文子串的意思就是这个字符串中最长的身为回文串的子串啦~” 小Ho道:“原来如此!那么我该怎么得到这些字符串呢?我又应该怎么告诉你我所计算出的最长回文子串呢? 小Hi笑着说道:“这个很容易啦,你只需要写一个程序,先从标准输入读取一个整数N(N<=30),代表我给你的字符串的个数,然后接下来的就是我要给你的那N个字符串(字符串长度<=10^6)啦。而你要告诉我你的答案的话,只要将你计算出的最长回文子串的长度按照我给你的顺序依次输出到标准输出就可以了!你看这就是一个例子。” 提示一 提示二 提示三 提示四 样例输入 3 abababa aaaabaa acacdas 样例输出 7 5 3


这个题目有很多解法,具体可以看这里,其中的中心扩展法很有意思,不过时间复杂度$$O(n^2)$$。后来发现Manacher算法时间复杂度只要$$O(n)$$,膜拜之,无奈研究了很久也没理解,很多中文博客写得不够全面详细,直到我看了这篇文章,明白了。主要思路如下: 1. 首先将字符串S预处理成T。在S的字符之间插入#字符,如下:
For example: S = “abaaba”, T = “#a#b#a#a#b#a#”.
这样之后,不管S长度是奇数还是偶数,得到的T都是奇数,这样就不需要分情况讨论了,也方便了后面的求解。 2. 我们将中间结果存储在数组P中:
T = # a # b # a # a # b # a # P = 0 1 0 3 0 1 6 1 0 3 0 1 0
P[i]表示以T[i]为中心,T中回文串的半径,比如T[3]=b,P[3]=3,表示以b为中心,T中回文串的半径为3,也就是#a#b#a#,半径就是#a#,而P[3]=3也正好是原始串S中以b为中心的回文串的总长度,即回文串aba的长度为3. 所以我们只需要求出数组P,然后求P中的最大值即为S中最长回文串的长度。 3. 为了求数组P,有两种情况需要讨论。假设我们已经求出一部分数组P的值了,如下图所示: C为目前P中最大值的位置,C为目前T中使得R推进到最右边的回文串的中心位置center(不一定是P的最大值的位置,最大值还需要在最后一步求解),它的半径是9,所以左边(Left)到达L,右边(Right)到达R,下标分别是2和20. 4. 第一种情况是,如果此时i=13,则P[13]等于多少呢? i的对称点i’下标为9,P[i’]=1,它的回文串没有超出以C为中心的回文串,根据对称性,则P[i]=P[i’]=1. 5. 第二种情况是,如果i=15,则P[15]等于多少呢? 由图可知,i的对称点i’=7,P[i’]=7,即以i’为中心的回文串已经超出了以C为中心的回文串,L左边的红色线条即为超出部分。那么此时不能说P[i]=P[i’]=7,因为如果这样的话,那么以C为中心的回文串也可以扩展到红色线条部分,这样P[C]就大于原来的P[C]了。但是可以肯定的是,i的半径至少是R-i=5,所以我们可以在此基础上依次增加i的半径,判断是否还是回文串。 6. 总结一下就是:
if P[ i’ ] ≤ R – i, then P[ i ] ← P[ i’ ] else P[ i ] ≥ P[ i’ ]. (Which we have to expand past the right edge (R) to find P[ i ].
更详细的解释和部分代码实现请看原博文,下面是我的针对hihoCoder #1032 : 最长回文子串提交的代码: [cpp] #include&lt;iostream&gt; #include&lt;string&gt; using namespace std; //字符串预处理,插空#,首尾分别^&amp; string pre_process(string s) { string rs; int s_len=s.size(); if(s_len==0) return "^&amp;"; rs="^"; for(int i=0;i&lt;s_len;i++) //rs+="#"+s[i];//const char* 和int/char不能相加,指针越界 rs+="#"+s.substr(i,1);//const char*和string可以相加 return rs+"#&amp;"; } int get_longest_pal_len(string s) { string T=pre_process(s); int n=T.size(); int *P=new int[T.size()]; P[0]=0; int C=0,R=0; for(int i=1;i&lt;n-1;i++) { int i_mirror=2*C-i;//i的对称位置 P[i]=(R&gt;i)?min(R-i,P[i_mirror]):0;//如果R&gt;i,说明i还在R里面,如果P[i_mirror]&lt;=R-i,则类似图中i=13;如果P[i_mirror]&gt;R-i,则P[i]至少是R-i的,然后从R-i递增测试 while(T[i+1+P[i]]==T[i-1-P[i]]) P[i]++; if(i+P[i]&gt;R)//更新最大的中心点和右边界 { C=i; R=i+P[i]; } } int max_len=0; for(int i=1;i&lt;n-1;i++) { if(P[i]&gt;max_len)//寻找最大P[i],P[i]既是T[i]的回文串半径(包含#),也是以S[i]为中心的最长回文串长度。 max_len=P[i]; } delete[] P; return max_len; } int main() { int n; string s; cin&gt;&gt;n; while(n–) { cin&gt;&gt;s; cout&lt;&lt;get_longest_pal_len(s)&lt;&lt;endl; } return 0; } [/cpp] 这段代码有一小部分需要注意一下,就是string pre_process(string s);字符串预处理函数中的rs+=”#”+s.substr(i,1);不要写成了rs+=”#”+s[i];,因为”#”会隐式转换成const char*类型,而s[i]是char/int类型,”#”+s[i]相当于指针增加一个int偏移量,会导致指针越界。所以要写成”#”+s.substr(i,1),因为string.substr返回的是string类型,const char*和string是可以相加(连接)的。看来我还是基础不牢啊~ ]]>

hihoCoder week10-1-后序遍历

hihoCoder week10-1-后序遍历 题目1 : 后序遍历 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 在参与过了美食节之后,小Hi和小Ho在别的地方又玩耍了一阵子,在这个过程中,小Ho得到了一个非常有意思的玩具——一棵由小球和木棍连接起来的二叉树! 小Ho对这棵二叉树爱不释手,于是给它的每一个节点都标记了一个标号——一个属于A..Z的大写字母,并且没有任意两个节点的标号是一样的。小Hi也瞅准了这个机会,重新巩固了一下小Ho关于二叉树遍历的基础知识~就这样,日子安稳的过了两天。 这天,小Ho正好在求解这棵二叉树的前序、中序和后序遍历的结果,但是却在求出前序遍历和中序遍历之后不小心把二叉树摔到了地上,小球和木棍等零件散落了一地! 小Ho损失了心爱的玩具,正要嚎啕大哭起来,所幸被小Hi发现了,劝说道:“别着急,这不是零件都还在么?拼起来不就是了?” “可是我忘记了二叉树长什么样子了!”小Ho沮丧道。 “这个简单,你不是刚刚求出了这棵二叉树的前序和中序遍历的结果么,利用这两个信息就可以还原出整棵二叉树来哦!” “这样么?!!”小Ho止住了泪水,问道:“那要怎么做呢?” 没错!小Ho在这一周遇到的问题便是:给出一棵二叉树的前序和中序遍历的结果,还原这棵二叉树并输出其后序遍历的结果。 提示:分而治之——化大为小,化小为无 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为一个由大写英文字母组成的字符串,表示该二叉树的前序遍历的结果。 每组测试数据的第二行为一个由大写英文字母组成的字符串,表示该二叉树的中序遍历的结果。 对于100%的数据,满足二叉树的节点数小于等于26。 输出 对于每组测试数据,输出一个由大写英文字母组成的字符串,表示还原出的二叉树的后序遍历的结果。 样例输入 AB BA 样例输出 BA


解法一(已更新简洁版解法二) 简单来说就是根据前序遍历和中序遍历的结果求得后序遍历。这个题目如果用手算的话还简单些,但是要用程序实现,还真让我无从下手。不过可以明确的思路是:依次取前序遍历中的一个元素,找它在中序遍历中的位置,然后把中序遍历分成左右两部分,再在左右两部分递归求解。 递归的思路虽然简单,具体代码实现时有很多细节需要注意,我参考了这篇博文里面的用前序遍历和中序遍历重建二叉树的代码。但是代码好像有一点小问题,指针横飞,我根据《数据结构》P161整理如下: [cpp] #include<iostream> #include<string> #include<cstdlib> using namespace std; typedef struct node_t { char data; struct node_t* lchild; struct node_t* rchild; }node;//树的定义 //根据先序遍历和中序遍历还原树结构 void rebuild_tree_from_pre_in_order(string &pre_order,string in_order,int tree_len,node*& root)//注意node*&要带上&,表示node*root的引用,看数据结构P162 { if(pre_order==""||in_order=="") return; node *tmp=(node*)malloc(sizeof(node)); tmp->data=pre_order[0]; tmp->lchild=NULL; tmp->rchild=NULL; if(root==NULL) root=tmp;//这里改变了root指针的指向,所以传进来的时候要node*& root使用引用! if(tree_len==1) { pre_order=pre_order.substr(1); //每次取前序遍历的元素之后都要将其删除,并且要将结果带出去 return; } int i=0,left_len=0,right_len=0; while(pre_order[0]!=in_order[i])//找到先序遍历中的根在中序遍历中的位置,将中序遍历一分为二 i++; left_len=i; right_len=in_order.size()-i-1; pre_order=pre_order.substr(1);//每次取先序遍历的元素之后都要将其删除,如果tree_len!=1,则要先计算了left_len和right_len之后再删除 if(left_len>0) rebuild_tree_from_pre_in_order(pre_order,in_order.substr(0,left_len),left_len,root->lchild);//递归左子树 if(right_len>0) rebuild_tree_from_pre_in_order(pre_order,in_order.substr(i+1,right_len),right_len,root->rchild);//递归右子树 } //后序遍历 void post_order_print(node* root) { if(root!=NULL) { post_order_print(root->lchild); post_order_print(root->rchild); cout<<root->data; } } int main() { string pre_order,in_order; cin>>pre_order>>in_order; node* root=NULL; rebuild_tree_from_pre_in_order(pre_order,in_order,pre_order.size(),root); post_order_print(root); return 0; } [/cpp] 首先利用递归的思想重建二叉树,然后后序遍历输出。需要注意的是我把二叉树节点统一定义为node,重建函数 [cpp] void rebuild_tree_from_pre_in_order(string &pre_order,string in_order,int tree_len,node*& root) [/cpp] 传入的参数需要写成node*& root,也就是node* root指针的引用,如果是C语言里可以用二级指针,因为在这个函数内部对指针node* root进行了修改(if(root==NULL) root=tmp;),也就是改变了上一个node里的 node_t* lchild和node_t* rchild的指向,这是改变了指针的指向(指针本身的内容),所以需要传入指针的引用,如果只是想改变指针指向的内容node->data,可以只用node*,总之一句话,想改变什么就传入什么的指针,想改变int就传入int*或者int&,想改变node*就传入node**或者node*&,这个在链表里也有类似的应用。 还有一个通俗易懂的例子: [cpp] #include<stdio.h> #include<stdlib.h> #include<string.h> void get_memory(char*& buffer,int n) { buffer=(char*)malloc(sizeof(char)*n); } int main() { char *str=NULL; get_memory(str,20); strcpy(str,"Hello World!"); printf("%s",str); return 0; } [/cpp] 如果void get_memory(char*& buffer,int n);的形参buffer不是传入指针的引用的话,函数里malloc分配的内存只是分配给了char*的副本,并不会传出去,这样会导致内存泄露。 解法二(2015.4.16更新) 今天回头来看解法一真是笨拙,解法一先恢复了树的结构,然后求树的后序遍历。其实可以不用这么复杂,题目的提示已经很明确了,不知道当时为什么没有注意到。 具体解法是这样的,假设s,t分别为前序遍历和中序遍历,则s=根+左+右;t=左+根+右;根据s可以知道根节点,在t中查找根节点,可以把t分成左和右,因为s和t的左右孩子长度是一样的,所以也可以把s分成左和右。这样我们就分别有了左孩子和右孩子的前序和中序遍历,又可以递归上述过程。 通过这种方法,我们可以由前序和中序遍历直接得到后序遍历。具体代码如下: [cpp] #include<iostream> #include<string> using namespace std; //s前序,t中序 string get_post_order(string s,string t) { if(s=="") return ""; if(s.size()==1) return s; int i; for(i=0;i<t.size();i++) if(t[i]==s[0]) break; string s_left=s.substr(1,i); string s_right=s.substr(i+1);//如果i+1==长度,则返回空串,当i+1>长度时才抛出异常。 string t_left=t.substr(0,i); string t_right=t.substr(i+1); return get_post_order(s_left,t_left)+get_post_order(s_right,t_right)+s[0]; } int main() { string s,t; while(cin>>s>>t) cout<<get_post_order(s,t)<<endl; return 0; } [/cpp] 由于题目已经过期,没办法提交到hiho,我简单测试了几个例子,得到了正确的结果。]]>

hihoCoder 1015-KMP算法

hihoCoder 1015-KMP算法 #1015 : KMP算法 时间限制:1000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进。 这一天,他们遇到了一只河蟹,于是河蟹就向小Hi和小Ho提出了那个经典的问题:“小Hi和小Ho,你们能不能够判断一段文字(原串)里面是不是存在那么一些……特殊……的文字(模式串)?” 小Hi和小Ho仔细思考了一下,觉得只能想到很简单的做法,但是又觉得既然河蟹先生这么说了,就肯定不会这么容易的让他们回答了,于是他们只能说道:“抱歉,河蟹先生,我们只能想到时间复杂度为(文本长度 * 特殊文字总长度)的方法,即对于每个模式串分开判断,然后依次枚举起始位置并检查是否能够匹配,但是这不是您想要的方法是吧?” 河蟹点了点头,说道:”看来你们的水平还有待提高,这样吧,如果我说只有一个特殊文字,你能不能做到呢?“ 小Ho这时候还有点晕晕乎乎的,但是小Hi很快开口道:”我知道!这就是一个很经典的模式匹配问题!可以使用KMP算法进行求解!“ 河蟹满意的点了点头,对小Hi说道:”既然你知道就好办了,你去把小Ho教会,下周我有重要的任务交给你们!“ ”保证完成任务!”小Hi点头道。 提示一:KMP的思路 提示二:NEXT数组的使用 提示三:如何求解NEXT数组 输入 第一行一个整数N,表示测试数据组数。 接下来的N*2行,每两行表示一个测试数据。在每一个测试数据中,第一行为模式串,由不超过10^4个大写字母组成,第二行为原串,由不超过10^6个大写字母组成。 其中N<=20 输出 对于每一个测试数据,按照它们在输入中出现的顺序输出一行Ans,表示模式串在原串中出现的次数。 样例输入 5 HA HAHAHA WQN WQN ADA ADADADA BABABB BABABABABABABABABB DAD ADDAADAADDAAADAAD 样例输出 3 1 3 1


题目大意就是用KMP算法求解一个子串在主串中出现的次数。我们都知道常规KMP算法可以用来判断一个模式串t是否在主串s中出现过,以及出现的位置。但是这题要求我们求出出现的次数。我们只需要对KMP算法稍加修改即可。 KMP的详细介绍可以看这里,我是仔细看了学校发的李春葆版本《数据结构》P105,代码也基本上照搬课本,使用的是修正后的求nextval数组的KMP算法。具体代码如下: [cpp] #include<iostream> #include<string> using namespace std; int nextval[10001]; //改进的next数组求法 void get_nextval(const string &t) { int j=0,k=-1; nextval[0]=-1; int t_len=t.size(); while(j<t_len) { //t[k]表示前缀,t[j]表示后缀 if(k==-1||t[j]==t[k]) { j++; k++; //较之前next数组求法,改动在下面4行 if(t[j]!=t[k]) nextval[j]=k; //之前只有这一行 else nextval[j]=nextval[k]; // 继续递归 } else k=nextval[k]; // 递归往前找 } } //t为模式串,s为主串;返回t在s中出现的次数 int get_show_times_kmp(const string &t,const string &s) { int t_len=t.size(),s_len=s.size(),i=0,j=0; int times=0; //int *nextval=new int[t_len]; get_nextval(t); while(i<s_len) { if(j==-1||s[i]==t[j]) { i++; j++; } else j=nextval[j]; if(j==t_len) { times++; //i=i-t_len+1; //j=0;//如果匹配成功的话,无需从t的开头重新匹配,也是只要将t右滑j-k个位置。看《数据结构》P107 } } return times; } int main() { int n; string t,s;//t为模式串,s为主串 cin>>n; while(n–) { cin>>t>>s; cout<<get_show_times_kmp(t,s)<<endl; } return 0; } [/cpp] 我做的修改只有一处,就是KMP搜索的时候,如果j==t_len说明在主串s中找到了一个t的匹配,令times++,此时程序还不能结束,我们要让匹配继续下去,为了让while成立,修改while条件只有i<s_len。 其实这个思路是我后来看某大神的解法思考过后才想明白的。我最开始的想法是这样的:既然在t.j和s.i处匹配成功,要进行下一轮匹配时,就应该令j=0,i=i-t_len+1;,也就是从主串s匹配成功起始位置的下一个位置和模式串t的起始位置开始重新匹配。这样做虽然正确,但是TLE超时。后来我仔细看课本P107图4.10,假设$$t=t_0t_j$$ ,且$$t_j==s_i$$,因为$$t_0…t_{k-1}==t_{j-k}…t_{j-1}$$,所以如果要进行下一次匹配,也可以直接将$$t_0…t_{k-1}$$右滑到$$t_{j-k}…t_{j-1}$$的位置,也就是$$t_0…t_{k-1}==s_{i-k}…s_{i-1}$$,再依次判断$$s_i$$是否等于$$t_k$$。这其实就是令j=nextval[j]。所以当j==t_len的时候,不需要特别处理,直接下一次循环就好。这样可以节省很多时间。 以上代码在hiho中AC,用时226ms,内存7MB。]]>