程序员面试金典——解题总结: 9.18高难度题 18.13给定一份几百万个单词的清单,设计一个算法,创建由字母组成的最大矩形

#include <iostream>
#include <stdio.h>
#include <hash_map>
#include <string>
#include <vector>
#include <fstream>
#include <algorithm>
#include <sstream>

using namespace std;
/*
问题:给定一份几百万个单词的清单,设计一个算法,创建由字母组成的最大矩形,其中每一行组成一个单词(自左向右),每一列也组成一个单词(自上而下)。
     不要求这些单词在清单里连续出现,但要求所有行等长,所有列等高。
分析:等于每一行和每一列都要是一个单词,这个只能通过递归去做了。
书上做法:
步骤1:先从字典中选择长度最长的单词记为L,那么单词矩阵最大面积为s=L*L,初始以s,行长度i从L到1,列高度j为s/L,构建长度i高度为j的单词矩阵
                 以该长度i摆放在单词矩阵第一行,然后从等长度的单词数组中选择下一个尝试进行 摆放
步骤2:摆放后,如果摆放完成(单词矩阵的高度等于所求高度):判断单词矩阵每一行列的字符串是否在字典中出现,如果都出现,说明是的 ; 否则令单词矩阵面积s减1,转步骤1
		      如果没有摆放完成,遍历现有单词矩阵每一列,判断每一列组成的字符串是否在等长度的所有单词生成的trie树中为前缀,
			      如果都是,继续摆放下一个单词,转步骤2;
				  否则,说明当前摆放的单词不对,尝试用其他等长度单词继续摆放该位置,如果所有等长度单词都尝试过,且都不行,那么说明该单词矩阵无法生成,令
				        单词矩阵面积s减1,转步骤1
			                                                        
*/

const int MAXSIZE = 1024;

//前缀树结点包含:孩子结点指针数组,从根节点到当前结点是否是一个字符串的标记【结点的字符,体现在孩子结点指针数组的下标上,下标index = 字符 - 'a'】
const int BRANCH_NUM = 26;//26个英文字符
class TrieNode
{
public:
	TrieNode()
	{
		_isString = false;
		memset(_childs , NULL , sizeof(_childs) );
	}
	bool _isString;
	TrieNode* _childs[BRANCH_NUM];
};

//前缀树包含了:根节点,查找,删除,销毁等成员和函数
class TrieTree
{
public:
	TrieTree():_root(NULL){}//初始化根节点为空,否则后续析构会有问题
	~TrieTree()
	{
		deleteTrie(_root);
	}
	void insert(vector<string>& strs)
	{
		if(strs.empty())
		{
			return;
		}
		int size = strs.size();
		string word;
		for(int i = 0 ; i < size ; i++)
		{
			if(i == 27)
			{
				int a = 1;
			}
			word = strs.at(i);
			insert((char*)word.c_str());
		}
	}

	//判断字符串中是否含有除'a'~'z'之外的其他字符
	bool containInvalidChar(char* str)
	{
		if(NULL == str)
		{
			return true;
		}
		char* tempStr = str;
		bool isInvalid = false;
		while( (*str) != '\0')
		{
			int index = (*str) - 'a';
			if(index < 0 || index >= BRANCH_NUM)
			{
				isInvalid = true;
				break;
			}
			str++;
		}
		str = tempStr;
		return isInvalid;
	}

	//插入一个字符串:遍历每个字符,插入到对应子树上,当所有字符处理玩,这只最后一个字符对应结点的字符串标记为真
	void insert(char* str)
	{
		if(NULL == str || NULL == _root)
		{
			return;
		}
		//包含无效字符,则不建立
		if(containInvalidChar(str))
		{
			return;
		}
		TrieNode* curNode = _root;
		while( (*str) != '\0')
		{
			//找到对应子树
			int index = (*str) - 'a';
			//"self-satisfaction",里面不能出现其他字符;过滤的时候,str必须累加。 这里最终决定出现其他字符,就不建立“前缀”
			if(index < 0 || index >= BRANCH_NUM)
			{
				str++;
				continue;
			}
			//寻找对应子树,如果为空,就新建子树对应结点
			TrieNode* childNode = curNode->_childs[index];
			if(NULL == childNode)
			{
				TrieNode* node = new TrieNode();
				curNode->_childs[index] = node;
			}
			str++;
			//设置当前结点继续往下走
			curNode = curNode->_childs[index];
		}
		//处理完成后,设置最后结点对应的字符串标记为真
		curNode->_isString = true;
	}

	//查找字符串是否为前缀树中的某个前缀
	bool searchPrefix(char* str)
	{
		if(NULL == str || NULL == _root )
		{
			return false;
		}
		TrieNode* curNode = _root;
		while( (*str) != '\0')
		{
			int index = (*str) - 'a';
			if(index < 0 || index >= BRANCH_NUM)
			{
				return false;
			}
			//寻找对应子树,如果为空,就说明没有找到
			TrieNode* childNode = curNode->_childs[index];
			if(NULL == childNode)
			{
				return false;
			}
			str++;
			curNode = childNode;
		}
		//如果字符串最后为空了,说明必定是查找到整个前缀了,返回为真
		if((*str) == '\0')
		{
			return true;
		}
		return false;
	}

	//查找某个字符串:对每个字符继续向下遍历对应的结点,直到字符串为空时,此时如果最后一个字符对应的结点的字符串标记如果为真,就说明查找到
	//这个是查找某个字符串是否存在的
	bool search(char* str)
	{
		if(NULL == str || NULL == _root )
		{
			return false;
		}
		TrieNode* curNode = _root;
		while( (*str) != '\0')
		{
			int index = (*str) - 'a';
			//一旦出现不属于'a'~'z'的字符,将不进行查询,直接返回错误
			if(index < 0 || index >= BRANCH_NUM)
			{
				return false;
			}
			//寻找对应子树,如果为空,就说明没有找到
			TrieNode* childNode = curNode->_childs[index];
			if(NULL == childNode)
			{
				return false;
			}
			str++;
			curNode = childNode;
		}
		return curNode->_isString;
	}

	//删除树,递归删除:先递归,最后删除结点
	void deleteTrie(TrieNode* root)
	{
		if(NULL == root)
		{
			return;
		}
		for(int i = 0 ; i < BRANCH_NUM ; i++ )
		{
			if( root->_childs[i] != NULL )
			{
				deleteTrie(root->_childs[i]);
			}
		}
		delete root;
		root = NULL;
	}

	TrieNode* _root;
};

//单词数组:包含了:最大单词的长度,以长度进行分组的单词数组
class WordGroup
{
public:
	WordGroup(int maxLength):_maxLength(maxLength){}

	//读取所有单词,按长度分组,分别存储至对应长度的单词数组中
	void getWordGroup(hash_map<string , int>& words , vector< vector<string> >& wordGroups)
	{
		if(_maxLength <= 0 || words.empty())
		{
			_wordGroups = wordGroups;
			return ;
		}
		//先创建一个长度为 _maxLength的数组
		for(int i = 0 ; i < _maxLength ; i++)
		{
			vector<string> strings;
			wordGroups.push_back(strings);
		}
		hash_map<string , int>::iterator it = words.begin();
		string word;
		int length = 0;
		for( ; it != words.end() ; it++)
		{
			word = it->first;
			length = word.length();
			//存入到对应长度的单词数组中
			wordGroups.at( length - 1 ).push_back(word);
		}
		_wordGroups = wordGroups;
	}

	vector<string> getWords(int length)
	{
		vector<string> words;
		if(_wordGroups.empty() || length > _maxLength)
		{
			return words;
		}
		words = _wordGroups.at(length - 1);
		return words;
	}
public:
	int _maxLength;
	vector< vector<string> > _wordGroups;
};

//读取字典中每一行,截取出英文单词,存放到哈希表中; 寻找出长度最长的单词,并将单词按照长度分组
int readDictionary(string& fileName , hash_map<string , int>& words)
{
	ifstream infile(fileName , ios::in);
	if(!infile)
	{
		cout << fileName << " can not open" << endl;
		return -1;
	}
	char line[MAXSIZE];
	int index = -1;
	string word;
	int maxLength = INT_MIN;
	while(!infile.eof())
	{
		infile.getline(line , MAXSIZE);
		//找到"|"截取出前面的部分作为单词
		string sLine(line);
		if(sLine.empty())
		{
			continue;
		}
		index = sLine.find("|");
		word = sLine.substr(0 , index);
		//将单词转换为小写
		transform(word.begin() , word.end() , word.begin() , tolower);
		if(words.find(word) == words.end())
		{
			words.insert(pair<string , int>(word , 1));
		}
		int length = word.length();
		if(length > maxLength)
		{
			maxLength = word.length();
		}
	}
	infile.close();
	return maxLength;
}

//单词形成的矩形:包含矩阵的长度和高度,长度由传入的单词长度确定,需要一个二维的字符数组
const int CHAR_MAXSIZE = 100;
class Rectangle
{
public:
	Rectangle(int length):_length(length),_height(0),_isValid(true){}

	bool isRepeated(string& word)
	{
		//如果能找到,说明重复
		if(_wordToTimes.find(word) != _wordToTimes.end())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	//向已有单词矩形中添加一个单词,返回添加后的单词矩阵
	Rectangle append(string& word)
	{
		if(word.empty() || word.length() != _length)
		{
			return *this;
		}
		if(isRepeated(word))
		{
			return *this;
		}
		_wordToTimes[word] = 1;
		_words.push_back(word);
		/*
		for(int i = 0 ; i < length ; i++)
		{
			char value = word.at(i);
			_charMatrix[_height][i] = value;
		}
		*/
		_height++;
		return *this;
	}

	//删除单词矩形中最后一行,用于回溯;不是实际删除,只是设置高度减少
	void remove()
	{
		if(_height <= 0)
		{
			return;
		}
		_words.pop_back();
		_height--;
	}

	//获取一列的值:行号一直变化,列号不懂
	string getColumn(int columnNum)
	{
		stringstream stream;
		for(int i = 0 ; i < _height ; i++)
		{
			//char ch = _charMatrix[i][columnNum];
			char ch = _words.at(i).at(columnNum);
			stream << ch ;
		}
		string word = stream.str();
		return word;
	}

	//判断已有单词矩阵是否摆放完成:需要对每一列,检查该列的字符串是否在字典中,每一行无需检查,因为之前摆放的每一行都是字典中的单词
	bool isCompleted(int length , int height , hash_map<string ,int>& dictionary)
	{
		string word;
		for(int i = 0 ; i < length ; i++)
		{
			word = getColumn(i);
			if( dictionary.find(word) == dictionary.end() )
			{
				return false;
			}
		}
		return true;
	}

	//检查当前字符串矩阵部分是不是OK,仍然是对每一列,检查每一列的字符串是否在前缀树trie中,如果不在,说明无法构建
	bool isPartialOK(int length  , TrieTree* trieTree)
	{
		//如果高度为0,说明初始建立单词矩阵,返回为真
		if(_height == 0)
		{
			return true;
		}
		if(NULL == trieTree || length <= 0)
		{
			return false;
		}
		string word;
		for(int i = 0 ; i < length ; i++)
		{
			word = getColumn(i);
			//这里是查找前缀是否存在
			if(!trieTree->searchPrefix((char*)word.c_str()))
			{
				return false;
			}
		}
		return true;
	}
public:
	int _length;
	int _height;//记录当前又有的字符矩形高度
	bool _isValid;//是否是有效的矩形
	//char _charMatrix[][CHAR_MAXSIZE];//这个应该可以用字符串替换的
	vector<string> _words;
	hash_map<string ,int> _wordToTimes;//用于判断添加的字符串是否重复
};



//判断是否能够形成长度为length ,高度为height的单词矩阵
Rectangle formRectangle(int length , int height , WordGroup& wordGroup , Rectangle& rectangle , hash_map<string ,int>& dictionary , TrieTree* trieTree)
{
	Rectangle nullRect(length);
	nullRect._isValid = false;
	if(length <= 0 || height <= 0)
	{
		return nullRect;
	}

	//判断是否完成,这里应该是摆放完成的,如果失败需要回溯吗?应该需要
	if(rectangle._height == height)
	{
		//如果摆放完成,直接返回该矩形
		if(rectangle.isCompleted(length , height , dictionary))
		{
			return rectangle;
		}
		//摆放失败,则需要回溯
		else
		{
			return nullRect;
		}
	}
	//判断部分的单词矩阵是否ok
	else
	{
		if(!rectangle.isPartialOK(length , trieTree))
		{
			return nullRect;
		}
	}

	//递归处理,获取等长的单词集合
	vector<string> words = wordGroup.getWords(length);
	int size = words.size();
	string word;
	for(int i = 0 ; i < size; i++ )
	{
		//加入一个单词,添加的单词不能重复
		word = words.at(i); 
		//如果该单词已经存在,则过滤
		if(rectangle.isRepeated(word))
		{
			continue;
		}
		//Rectangle newRect = rectangle.append(word);
		Rectangle newRect(rectangle);
		//确保原来的矩形不受影响,等同于起到回溯的作用
		newRect.append(word);
		Rectangle rect = formRectangle(length , height , wordGroup , newRect,dictionary , trieTree);
		if(rect._isValid)
		{
			return rect;
		}
	}
	return nullRect;
}

//生成单词矩阵
Rectangle formWordMatrix(int maxLength , WordGroup& wordGroup , hash_map<string ,int>& dictionary)
{
	TrieTree* trieArray = new TrieTree[maxLength];//设定一个trie树,默认
	//vector<TrieTree> trieArray;
	for(int i = 0 ; i < maxLength ; i++)
	{
		TrieTree trie;
		trieArray[i] = trie;
		//trieArray.push_back(trie);
	}
	int area = maxLength * maxLength;
	for(int s = area ; s > 0 ; s--)
	{
		//i表示单词的宽度
		for(int i = maxLength ; i >= 1 ; i--)
		{
			int j = s / i;//j表示单词的高度
			cout << "面积:" << s << ",长度:" << i << ",高度:" << j << endl;
			if( i == 13 && j == 30 && s == 400)
			{
				int kk = 1;
			}
			//尝试形成:长度为i,高度为j的单词矩阵
			vector<string> words = wordGroup.getWords(i);
			//如果同等长度的单词数组个数 < 单词矩阵的高度,则肯定无法建立
			if( words.size() < j )
			{
				continue;
			}
			//如果此时的对应长度的trie树还没有建立,就先建立。易错,这里对应的是 i-1的trie树
			if(trieArray[i-1]._root == NULL)
			{
				TrieNode* root = new TrieNode();
				trieArray[i-1]._root = root;
				trieArray[i-1].insert(words);
			}
			//下面就开始建立单词矩阵
			Rectangle initRect(i);
			Rectangle rect = formRectangle(i , j , wordGroup ,initRect , dictionary , &trieArray[i-1]);
			if(rect._isValid)
			{
				delete[] trieArray;
				return rect;
			}
		}
	}
	delete[] trieArray;
	Rectangle nullRect(maxLength);
	nullRect._isValid = false;
	return nullRect;
}

void print(Rectangle& rect)
{
	if(rect._isValid)
	{
		int size = rect._words.size();
		for(int i = 0 ; i < size ; i++)
		{
			cout << rect._words.at(i) << endl;
		}
	}
	else
	{
		cout << "No result" << endl;
	}
}

void process()
{
    hash_map<string ,int> words;  
    string fileName("./dictionary.txt");  
    int maxLength = readDictionary(fileName , words);  
	if(maxLength <= 0)
	{
		cout << "Can't form word matrix" << endl;
		return ;
	}
	WordGroup wordGroup(maxLength);
	vector< vector<string> > wordMatrix;
	wordGroup.getWordGroup(words , wordMatrix);
	
	//单词按长度分组后,下面就开始递归处理了
	Rectangle rect = formWordMatrix(maxLength , wordGroup , words);
	print(rect);
}

int main(int argc , char* argv[])
{
	process();
	getchar();
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值