关于PE458(project euler 458 Permutations of Project)的思考

本文回顾了PE458的解题过程中遇到的问题,介绍了trie,AC自动机,自动机化简算法.

题意:给定project 这7个字母(等价于互不相同的7个字母),问长度为10^12的由这7个字母组成的串且串中任意连续7个字母中不包含所有的7个字母(即:任意连续7个字母不是project的排列)有多少个,答案对1e9取模.

尝试用AC自动机和矩阵二分解决问题:

首先想到的构造一个自动机,能够判断一个串是否满足要求.假定构造出来,我们可以构造转移矩阵tran[i][j],当在状态机j状态下输入某个字母并转移到i状态,tran[i][j]的值增加1.给定列向量x[0] = (1, 0, 0, ...)‘, x[i] = tran * x[i-1], 第一个分量的状态表示空串的状态.于是x[i][j]就表示长度为i,且状态是j的串的个数.考虑其中需要的状态,相加即可.

怎么构造这个自动机呢,当然用AC自动机了.也就是说,先构造一个trie,然后把7!个不合法的串插入到这个trie中.在构造的trie中,还有一些状态在接受输入时没有下一个状态,所以下一步是把这些状态填满.

考虑一个非法状态,无论输入是什么,下一个状态还是本身,因为只要某处出现project的排列了,无论后面的输入何如,这个串都是非法的.

考虑其它中间状态,这个状态对应一个字符串s,|s|>0 .我们只需要找出一个串t,|t| < |s|, t是s的一个前缀,同时t也是s的后缀,并且|t|最大化. 显然t是存在的,因为空串是满足要求的.t同时也是唯一的. 当s在输入字母l时,如果没有下一个状态,那么等价于t在输入字母l时的状态转移. 而关于t可以递归地构造.

int p = 0, q = 0;
sfx[1] = 1;
for (int i = 0; i < 7; ++i) if (trie[1][i])
{
	que[q++] = trie[1][i];
	sfx[trie[1][i]] = 1;
}
else
{
	trie[1][i] = 1;
}
while (p < q)
{
	int curr = que[p++];
	assert(danger[sfx[curr]] == 0);
	danger[curr] = danger[curr] || danger[sfx[curr]];
	if (danger[curr])
	{
		for (int i = 0; i < 7; ++i)
		trie[curr][i] = curr;
	}
	else
	{
		for(int i = 0; i < 7; ++i) if (trie[curr][i])
		{
			que[q++] = trie[curr][i];
			sfx[trie[curr][i]] = trie[sfx[curr]][i];
		}
		else
		{
			trie[curr][i] = trie[sfx[curr]][i];
		}
	}
}

其中状态1表示空串. 如果状态i对应的串是s那么状态sfx[i]的状态就是t.danger[i]表示该状态是非法的. trie[i][j] == 0的时候表示在输入j的时候trie没有边.可以看到这就是一个简单的bfs,再深入看看其实ac自动机就是kmp算法在多串上的扩展.

然后构造tran矩阵,矩阵二分可以计算出来x[1e12],把合法分量相加即可.

但是杯具的是,状态有13700个,n^3的矩阵乘法在一般机器上不可行.(在我机器上计算量为6.5204 * 10^12 的简单计算需要5个小时. 姑且认为矩阵乘法的每一个操作和我这个参考操作的计量时间一样,单个矩阵乘法的计算量是我的参数计算的40%不妨设为2小时.再考虑1e12对应的二进制大概是40位,总共需要80个小时的计算.如果再考虑两种基本操作的差异,缓存神马的,时间更不好估计了)

直接构造的自动机由于状态数太多,不可行,所以得想办法改.

尝试减少自动机的状态:

我总共采用了三种减少状态的方法.

首先是经典的自动机化简的算法.

用belong[i]表示i状态被化简后的状态.初始情况下,非法状态的值是2,合法状态状态的值是1.

给定一个无限循环:

用map<vector<int>, vector<int> > mem;来记录当前的转移情况.

对于状态i,key的第一个分量是belong[i],其它分量是belong[trie[i][0], belong[trie[i][1]],

..., belong[i][6]. value对应的vector包含i. 这样就通过key把所有的状态区分出来了.

然后再遍历一次这个map,对于一个key,为value对应的所有状态分配相同的belong值.

如果在分配前和分配后,状态数不发生变化,则算法终止.

for (int i = 1; i < top; ++i) if (danger[i] == 0)
{
	belong[i] = 1;
}
else
{
	belong[i] = 2;
}
int size = 2;
for (;;)
{
	map<vector<int>, vector<int> > mem;
	for (int i = 1; i < top; ++i)
	{
		vi key;
		key.pb(belong[i]);
		for (int j = 0; j < 7; ++j)
		key.pb(belong[trie[i][j]]);
		mem[key].pb(i);
	}

	int id = 1;
	for (auto& it: mem)
	{
		for (auto& s: it.second)
		belong[s] = id;
		++id;
	}
	if (mem.size() == size)
		break;
	size = mem.size();
}

算法其实很快.在coursera上的dfa课程中也给了个算法.算法的复杂度是输入状态数的平方,好处是给了一个有确定复杂度的算法,坏处是平方级的太慢,不如这样迭代.

在化简后,状态数为8661,仍然不可行.

其次是在构造自动机的时候可以试图减少一些状态.

考虑到所有的trie的叶子结点都是不合法的,可以在构造trie的时候把这些点视为一个,然后再使用自动机化简的算法.化简后状态数还是8661,反而验证了自动机存在一个最小状态数,也一定程度上说明我写的化简算法大概是正确的.

最后,不考虑trie,试图直接构造自动机,这样可以充分利用题目条件,有可能达到减少状态的目的.

考虑某个状态,对应于长度为l的串s,在面对输入i的时候,如果i不在s中出现,则将i追加到l,然后转移到一个新的状态.如果i在s中出现,设出现位置是pos,那么把s中从pos+1的串取出来,将i追加到这个取出来的串后面,并转移到这个状态.

struct Pt
{
	int s;	// 表示自动机中的状态
	vector<int> v;	// 表示这个状态对应的串
};
queue<Pt> last;
map<vector<int>, int> mem; // key为串,value为对应的状态
// last中放入一个初始状态,Pt.s = 1, Pt.v 为空
// 标记状态1为访问过
// 将空串加入到mem中
for (int len = 0; len <= 5; ++len)
{
	queue<Pt> next;
	对于last中每一个状态now:
		枚举输入字母i
			如果i不在now.v中出现,则转移到的串B, 如果B出现过则找到对应的状态,否则就分配新的状态
				并将状态加入到next队列中. 标志状态转移.
			如果i在now.v中出现,也能构造一个串B, B一定出现过, 标记状态转移.
	last.swap(next);
}
// last中的长度为6的状态,特殊处理长度为6的状态到长度为7的状态的转移

现在看一下状态数:

长度为0的:1

长度为1的:1!*C(7,1)=P(7,1)=7

长度为2的:2!*C(7,2)=P(7,2)=42

长度为3的:3!*C(7,3)=P(7,3)=210

长度为4的:4!*C(7,4)=P(7,4)=840

长度为5的:5!*C(7,5)=P(7,5)=2520

长度为6的:6!*C(7,6)=P(7,6)=5040

长度为7的:1

1+7+42+210+840+2520+5040+1=8661

如果不特殊处理长度为7的串则:

1+7+42+210+840+2520+5040+5040=13700

在化简的三个思路中得到了相同的结果,也再次验证了这个自动机的状态数的下界是8661.说明了对于这个自动机,状态数的化简只对非法结点产生了效果.同是第三次直接给出自动机的算法也更进一步说明了trie对应的AC自动机的内涵.

[其实在实现这个算法前我错误地估计了状态数,把后面的组合数弄掉了,长度为6的才720,再加上其它的也不超过1000,1000^3一次的矩阵乘法还是可行的]

正解是什么:

在上述碰壁后,我恍然大悟,茅塞顿开,删除了很多代码,保留了一些,补了不到十行代码,解决了这个问题.由于PE的游戏规则这里就不细说了.

时间: 2024-09-14 14:14:59

关于PE458(project euler 458 Permutations of Project)的思考的相关文章

Python练习题 048:Project Euler 021:10000以内所有亲和数之和

本题来自 Project Euler 第21题:https://projecteuler.net/problem=21 ''' Project Euler: Problem 21: Amicable numbers Let d(n) be defined as the sum of proper divisors of n (numbers less than n which divide evenly into n). If d(a) = b and d(b) = a, where a ≠ b

Python练习题 047:Project Euler 020:阶乘结果各数字之和

本题来自 Project Euler 第20题:https://projecteuler.net/problem=20 ''' Project Euler: Problem 20: Factorial digit sum n! means n × (n ? 1) × ... × 3 × 2 × 1 For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, and the sum of the digits in the number 10! i

Python练习题 046:Project Euler 019:每月1日是星期天

本题来自 Project Euler 第19题:https://projecteuler.net/problem=19 ''' How many Sundays fell on the first of the month during the twentieth century (1 Jan 1901 to 31 Dec 2000)? Answer: 171 ''' from datetime import * firstDay = date(1901,1,1) lastDay = date(

Python练习题 035:Project Euler 007:第10001个素数

本题来自 Project Euler 第7题:https://projecteuler.net/problem=7 # Project Euler: Problem 7: 10001st prime # By listing the first six prime numbers: # 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13. # What is the 10 001st prime number? # Answer

Python练习题 034:Project Euler 006:和平方与平方和之差

本题来自 Project Euler 第6题:https://projecteuler.net/problem=6 # Project Euler: Problem 6: Sum square difference # The sum of the squares of the first ten natural numbers is, # 1**2 + 2**2 + ... + 10**2 = 385 # The square of the sum of the first ten natur

Project Euler 126 - Cuboid layers

这题先是推公式- 狂用不完全归纳+二次回归,最后推出这么一个奇怪的公式 f(t,x,y,z)=4(t?1)(x+y+z+t?2)+2(xy+yz+xz) 表示长宽高为x .y .z 的立方体第t 层放的立方体的个数. 接下来就是算答案了- 方法很简单:暴力 但是暴力还是有技巧的,开始我是直接从1到1000枚举t .x .y .z ,但这样出不来结果. 换成下面代码里的方法就行了. 1 #include <iostream> 2 #include <cstdio> 3 #includ

Project Euler 第一题效率分析

Project Euler: 欧拉计划是一系列挑战数学或者计算机编程问题,解决这些问题需要的不仅仅是数学功底. 启动这一项目的目的在于,为乐于探索的人提供一个钻研其他领域并且学习新知识的平台,将这一平台打造一个有趣和休闲 的环境. 项目主页:https://projecteuler.net 第一题 Multiples of 3 and 5 If we list all the natural numbers below 10 that are multiples of 3 or 5, we ge

Python练习题 042:Project Euler 014:最长的考拉兹序列

本题来自 Project Euler 第14题:https://projecteuler.net/problem=14 ''' Project Euler: Problem 14: Longest Collatz sequence The following iterative sequence is defined for the set of positive integers: n → n/2 (n is even) n → 3n + 1 (n is odd) Using the rule

Python练习题 041:Project Euler 013:求和、取前10位数值

本题来自 Project Euler 第13题:https://projecteuler.net/problem=13 # Project Euler: Problem 13: Large sum # Work out the first ten digits of the sum of the following one-hundred 50-digit numbers. # Answer: 5537376230 numbers = '''371072875339021027987979982