蓝桥杯算法提高 -- 周期字串

思路:

相信大家都很容易想到, 根据字符串的长度, 求出所有约数, 然后按照约数的顺序来检验 . 但是检验的策略非常重要, 最重要的两点就是: 

(1)对每个不同长度周期的字符串, 最多只判断一次. 

(2)如果长度为N的字符串在原串的周期检验中不成立, 则长度为N的约数的字符串也不会成立 .


根据上述的结论, 我们可以大概感觉到, 我们不仅要求约数, 还要求约数的约数( 依次递归 ) , 而约数的约数本身又是原数的约数 , 至此, 可以断定使用多叉数结构(有向图)来进行约数存储( 我把它称为约数树 ). 


完全约数树: 如果根结点的所有约数都是根结点的下属结点(无论直接或间接), 则该树为完全约数树.


在贴代码之前, 我要对约数树的理论结构做出声明: 

(1) 描述整个问题域的约数树是一棵完全约数树, 但其所有子树都不一定是完全约数树( 这样可以避免约数重复而导致的周期判断重复 ).

(2) 从树根开始, 先序遍历约数树进行周期判断, 如果某结点判断失败, 根据策略2, 则其所有子树结点都将失败, 不必再检验.


以24为例, 给出如下约数树:


#include <iostream>
#include <cmath>
#include <memory.h>
#include <cstring>
using namespace std;
#define MAXN 110
/*约数树 : 子结点必然是根结点的约数, 但反之不成立*/

struct Edge{		// ...连接边 
	int		node;	// ...指向结点的下标 
	Edge	*next;	// ...下一条边 
}E[MAXN];			// ...所有边的集合 

struct CNode{			// ...约数树结点 
	Edge	*first;		// ...首边 
	int		minPossible;// ...该树范围内最小的可能周期( 留作剪枝用 ) 
	bool	isBuilt;	// ...表示该结点是否建树完毕 
}V[MAXN];				// ...所有点的集合 

int nEdgeCount = 0;		// ...当前已经使用的边的总量 
int strLen;				// ...原字符串总长度 
int minLen = MAXN;		// ...当前已知的最小正周期 
char ss[MAXN];			// ...原字符串 

// 添加一条从V[root]指向V[subRoot] 的边 
// 复杂度:	O(1) 
inline void AddEdge( int root, int subRoot ){
	Edge	*pE = E+nEdgeCount++;
	pE->next = V[root].first;
	pE->node = subRoot;
	V[root].first = pE;
}

// 建立约数树  
// 复杂度: O(?) 
void BuildTree( int root ){
	int Max = sqrt(root)+1;
	V[root].isBuilt = true;
	V[root].minPossible = root;
	V[root].first = NULL;
	for( int i = 2, j; i < Max; ++i )
	{
		if( root % i == 0 )
		{
			j = root/i;
			if( !V[j].isBuilt ) {
				AddEdge(root, j);
				BuildTree(j);
				V[root].minPossible = min(V[j].minPossible, V[root].minPossible);
			}
			if( !V[i].isBuilt ) {
				AddEdge(root, i);
				BuildTree(i);
				V[root].minPossible = min(V[i].minPossible, V[root].minPossible);
			}
		}
	}
}

// 匹配原串ss[0 ... length-1] 中是否存在长度为partLen的周期串
// 复杂度:	O(length) 
bool Match( int length, int partLen )
{
	static char* tmp[MAXN];
	int station = length / partLen; // 段 
	tmp[0] = ss;
	for( int i = 1; i < station; ++i ) tmp[i] = tmp[i-1]+partLen;
	for( int i = 0; i < partLen; ++i, ++tmp[0]){
		for( int j = 1; j < station; ++j ){
			if( *tmp[j] != *tmp[0] ) return false;
			++tmp[j];
		}
	}
	return true;
}

// 匹配 SS[ 0 ... root-1] 中的最小周期串
// 复杂度: O( root的约数总数 ) 
void Dfs_Match( int root )
{
	minLen = min(root,minLen);
	for( Edge *e=V[root].first; e != NULL; e = e->next )
		if( V[e->node].minPossible < minLen )//...最优性剪枝 
			if( Match(root, e->node) )
				Dfs_Match( e->node );
}

int main(int argc, char** argv) {	
	cin >> ss;
	strLen = strlen(ss);
	BuildTree(strLen);
	Dfs_Match( strLen );
	// Dfs中并没有检验是否有周期为1的可能性
	// 但如果该串的最小周期为1, 则其当前周期必然为2 
	if( minLen == 2 ) minLen = ss[0]==ss[1]?1:2;
	cout << minLen;
	return 0;
}



最后说两句: 

以约数树为基的检验策略似乎没有从根源上减低时间复杂度, 甚至因为建树而耗损了一些额外的时间, 但其的确大大减低了原复杂度的系数, 整个算法从根本上来说, 只是一种可行性剪枝, 但本人最后没有办法算出建树的复杂度,所以无法准确推敲. 

展开阅读全文

没有更多推荐了,返回首页