Programming Challenges 习题9.6.5

PC/UVa:110905/10029

Edit Step Ladders

如果一个单词可以通过增减、删除或者替换一个字母变成另一个单词,则两个单词构成递变(书中的翻译,英文原文是Edit Step)关系。给定一个字典(排好序),求字典中最长的字典序增加的递变序列。

第一步要找出哪些单词之间存在字典序的递变关系(构建图),第二步在图中找最长路径。

因为做这道题之前看了《数据结构与算法分析——C++语言描述(第四版)》,书中4.8.4节介绍了类似的算法:

  • 第一种是对给定的单词两两比较,O(n ^ 2)的算法,时间比较慢;
  • 第二种改进的是对长度进行分组,还是O(n ^ 2)的算法,但是时间提高很多

受刘汝佳的书的启发,将字符串转换为整数ID,这样可以减少字符串的比较查找操作。

算法上,这题用的第三种改进方式,在按照长度分组的基础上,对每一组中的每个单词,依次去掉每一个字母,将剩余部分作为键,原单词作为值,构造这样的一个键值对map<string, vector<size_t>。这样如果两个单词去掉一个字母后,剩余的部分相同,则它们就可以通过一次递变得到,例如finewine,在去掉fw后,剩下的是ine。要注意的是,一个单词去掉不同位置的字母后剩下的部分是一样的,所以vector中会有重复的。

书中介绍的算法到此就结束了,但是其实这有一个小bug,那就是不同单词去掉不同位置的一个字母后,剩下的部分也可能一样,例如atto

上述处理完毕后很容易得到长度相同的单词之间的递变关系,下面还需要长度差1之间的递变关系(题目中的增加和删除)。这可以借助上面构建的键值对。对于一个单词,查找是否有上面的键和它相同,如果有,那么就存在这种递变关系,注意添加这种单向边要满足字典序(不过不满足字典序也没关系,因为后边的第二步最长路径也有限制,但是加上限制而不是一味地插入运行会快一些,毕竟是set操作),这可以通过uDebug上的第一个例子测出来。

第二步是找最长路径,通过看别人的博客,发现可以变成最长上升子序列的问题。如果一前一后两个单词,存在上述单向边,则长度加1

另外,uDebug上Morass贡献的测试用例跑了很长世间,而且结果也不对,但是交到UVa上后AC了。小云儿提示测试用例可能有问题,仔细观察后发现,该测试用例中包含重复的单词,如果去掉重复的单词后再用uDebug跑,结果就一样了。

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>

using namespace std;

bool OneCharDiff(const string &str1, const string &str2)
{
	size_t cnt = 0;
	for (size_t i = 0; i < str1.size(); i++)
	{
		cnt += str1[i] == str2[i] ? 0 : 1;
		if (cnt > 1) return false;
	}
	return true;
}

void DSAA(const vector<string> &vecDic, const map<string, size_t> &mDic,
	vector<set<size_t>> &Graph)
{
	map<size_t, vector<size_t>> mLen2Word;
	vector<map<string, vector<size_t>>> vecmDel2Word;
	//先根据长度对单词分组
	for (auto word : vecDic)
	{
		mLen2Word[word.size()].push_back(mDic.find(word)->second);
	}
	string strTmp;
	vecmDel2Word.reserve(mLen2Word.size());
	for (auto iter = mLen2Word.begin(); iter != mLen2Word.end(); iter++)
	{
		vecmDel2Word.push_back(map<string, vector<size_t>>());
		map<string, vector<size_t>> &mDel2Word = vecmDel2Word.back();
		const vector<size_t> &wordID = iter->second;
		for (size_t ID : wordID)
		{
			for (size_t pos = 0; pos < iter->first; pos++)
			{
				strTmp = vecDic[ID];
				strTmp.erase(pos, 1);
				mDel2Word[strTmp].push_back(ID);
			}
		}
	}
	/*
	for (size_t idx = 0; idx < vecmDel2Word.size(); idx++)
	{
		const map<string, vector<size_t>> &mDel2Word = vecmDel2Word[idx];
		cout << "Origin length " << mDel2Word.begin()->first.size() + 1 << endl;
		for (auto iter = mDel2Word.begin(); iter != mDel2Word.end(); iter++)
		{
			cout << iter->first << ':';
			const vector<size_t> &wordID = iter->second;
			for (size_t ID : wordID)
			{
				cout << vecDic[ID] << ' ';
			}
			cout << endl;
		}
		cout << endl;
	}
	*/
	//构建单向边
	for (size_t idx = 0; idx < vecmDel2Word.size(); idx++)
	{
		//在相同的长度的ID之间构建单向边
		const map<string, vector<size_t>> &mDel2Word = vecmDel2Word[idx];
		for (auto iter = mDel2Word.begin(); iter != mDel2Word.end(); iter++)
		{
			if (iter->second.size() == 1) continue;
			const vector<size_t> &wordID = iter->second;
			for (size_t i = 0; i < wordID.size(); i++)
			{
				for (size_t j = i + 1; j < wordID.size(); j++)
				{
					if (OneCharDiff(vecDic[wordID[i]], vecDic[wordID[j]])){
						//不同的单词删除掉不同位置的字母剩下的内容可能是一样的
						//但是这两个不能通过一次递变互相转换
						//根据之前的处理可以保证字典序
						Graph[wordID[i]].insert(wordID[j]);
					}
				}
			}
		}
		//在长度差1的ID之间构建单向边
		if (idx < vecmDel2Word.size() - 1){
			size_t currlen = mDel2Word.begin()->first.size() + 1;
			const map<string, vector<size_t>> &mDel2Wordp1 = vecmDel2Word[idx + 1];
			if (mDel2Wordp1.begin()->first.size() == currlen){
				const vector<size_t> &wordID = mLen2Word.find(currlen)->second;
				for (auto ID : wordID)
				{
					auto iter = mDel2Wordp1.find(vecDic[ID]);
					if (iter != mDel2Wordp1.end()){
						for (size_t i = 0; i < iter->second.size(); i++)
						{
							if (vecDic[ID] < vecDic[iter->second[i]]){
								Graph[ID].insert(iter->second[i]);
							}
							if (vecDic[iter->second[i]] < vecDic[ID]){
								Graph[iter->second[i]].insert(ID);
							}
						}
					}
				}
			}
		}
	}
	/*
	for (size_t i = 0; i < Graph.size(); i++)
	{
		cout << vecDic[i] << ':';
		for (auto adj : Graph[i])
		{
			cout << vecDic[adj] << ' ';
		}
		cout << endl;
	}
	*/
}

int main()
{
	vector<string> vecDic;
	map<string, size_t> mDic;
	string strWord;
	size_t cnt = 0;
	while (cin >> strWord){
		if (mDic.find(strWord) == mDic.end()){
			vecDic.push_back(strWord);
			mDic[strWord] = cnt++;
		}
	}
	vector<set<size_t>> Graph(vecDic.size(), set<size_t>());
	DSAA(vecDic, mDic, Graph);
	vector<size_t> vecLen(Graph.size(), 1);
	for (size_t idx = 1; idx < Graph.size(); idx++)
	{
		size_t max = 1;
		for (size_t prev = 0; prev < idx; prev++)
		{
			auto iter = Graph[prev].find(idx);
			if (iter != Graph[prev].end()){
				if (max < vecLen[prev] + 1){
					max = vecLen[prev] + 1;
				}
			}
		}
		vecLen[idx] = max;
	}
	size_t maxlen = 0;
	for (auto len : vecLen)
	{
		if (len > maxlen) maxlen = len;
	}
	cout << maxlen << endl;
	return 0;
}
/*
cat
dig
dog
fig
fin
fine
fog
log
wine
*/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值