MT1310 最大乘积

题目

数组中有N个元素,找出乘积最大的连续子序列,输出他们的乘积。

格式

输入格式

第一行输入数组长度N,第二行输入数组元素,整型,空格分隔。

输出格式

输出整型。

样例 1

输入:
5  
2 5 -1 2 -1

输出:
20

程序


def main():
    #code here
    n = int(input())
    lisa = list(map(int,input().split()))
    max=1
    for i in range(0,len(lisa)):
        tmp =1
        tmp_max=1
        for j in range(i,len(lisa)):
            tmp*=lisa[j]
            if tmp_max<tmp:
                tmp_max=tmp
        if (max<tmp_max):
            max = tmp_max
      
    print(max)
    pass

if __name__ == '__main__':
    main();
<think>题目要求:寻找最长的子数组(连续),使得子数组内所有元素的乘积是一个完全平方数。如果不存在则输出-1。 重要观察: 乘积是一个完全平方数,意味着乘积的所有质因子的指数都是偶数。 我们可以将每个数进行质因数分解,然后将指数模2,这样每个数就可以表示为一个“特征值”(类似于一个向量,每个维度是0或1)。 那么,子数组的乘积为完全平方数,等价于子数组中所有数的特征值的按元素模2加法(即异或)为0。 具体方法: 1. 我们需要一种快速计算子数组乘积对应的特征向量的异或和的方法。 2. 注意到指数模2后,每个质因子的指数只有0或1,因此我们可以用一个二进制状态来表示整个数的特征。但是质因子可能很多(最大10^6以内,质因子个数最多也就十几个),我们可以将每个数用一个二进制数(状态)表示,其中每一位代表一个质因子的指数模2的结果(0表示偶数次,1表示奇数次)。 3. 然而,质因子个数很多(最多有10^6以内的质数),我们不可能用每一位对应一个质因子(那样状态空间太大)。因此,我们需要压缩状态:只考虑那些在数组元素分解中出现过的质因子?但是题目中数组长度10^6,每个数最大10^6,总共的质因子个数大约有不到80000个(10^6以内的质数个数),但是每个数分解后最多只有7~8个质因子(因为2*3*5*...*17已经大于10^6),所以我们不能直接按质因子个数来作为状态维度。 4. 替代方案:我们使用一个哈希表(或者字典)来记录每个状态第一次出现的位置(或者所有出现的位置),状态用一个整数表示,这个整数是由该数分解质因数后,将指数模2,然后只保留指数为奇数的质因子的乘积?不对,这样无法表示异或关系。 5. 更常用的技巧是:将每个数分解质因数,然后对于每个质因子,我们只关心指数模2的余数。那么,整个数的特征可以表示为:该数所包含的所有指数为奇数的质因子的异或(但这里不是异或,而是集合的对称差)?实际上,我们可以用一个二进制数,但是二进制数的每一位对应一个质因子。然而质因子太多,直接按质因子编号作为位的位置是不现实的(需要78498位,不可能)。 6. 另一种思路:我们只关心整个乘积的质因子指数奇偶性,而且两个数的乘积的奇偶性等于各自奇偶性的按位异或(模2加法)。因此,我们可以将每个数分解,然后得到其质因子奇偶性的状态(但是状态是一个集合)。那么,我们可以将每个质因子映射为一个唯一的位(比如按质因子的大小映射为0,1,2,...),但是这样状态空间可能很大(最多78498个质因子,但实际一个数分解后最多7~8个质因子,所以状态中只有很少的位是1)。 7. 实际上,我们可以利用:两个状态相同的数相乘,指数模2后相当于0(即完全平方数)。但这里我们考虑连续乘积,我们需要前缀积的异或值(这里用异或来表示模2加法)。 我们定义前缀状态数组pref[i] = a[0]的特征状态 XOR a[1]的特征状态 ... XOR a[i-1]的特征状态(即前i个元素的异或状态)。那么,子数组[l, r]的乘积对应的状态就是pref[r+1] XOR pref[l]。我们希望这个状态为0,即pref[r+1] = pref[l]。 因此,问题转化为:找到一个最长的区间[l, r]使得pref[r+1] = pref[l],然后区间长度就是(r+1)-l。 8. 具体步骤: a. 预处理每个数的质因数分解(快速分解,使用最小质因子法)。 b. 计算每个数的特征状态:对该数分解质因数,将指数模2,然后对于指数为奇数的质因子,将其对应的哈希值(用一个随机映射的整数来代表该质因子,或者用质因子的编号)进行异或。注意:这里我们不需要一个很大的位向量,而是用一个整数来表示状态,这个整数是每个质因子对应的一个哈希值的异或和。然而,为了避免冲突,我们可以直接使用质因子的编号,然后用一个很大的位宽(比如64位整数)来存储异或结果?但是质因子的编号可能达到78498,64位整数只能表示64位,不够。 9. 改进:我们不需要显式地给每个质因子一个唯一的编号,因为状态只用于比较是否相等。我们可以为每个质因子分配一个随机的64位整数(或者128位)作为它的代表值,然后每个数的状态就等于它所有指数为奇数的质因子的代表值的异或。这样,两个状态相等当且仅当对应的质因子集合的对称差为空(即两个状态代表的质因子集合相同且每个质因子的奇偶性一致)。由于随机分配,冲突概率很低(在n=10^6的情况下,冲突概率可以忽略)。 10. 步骤: - 首先,生成一个全局的随机映射表:对每个可能出现的质数p(最大到10^6),分配一个随机的64位整数(或者128位)作为hash值,记为hash[p]。 - 然后,对于每个数a_i,分解质因数,并将指数模2,得到指数为奇数的质因子集合,然后计算这个数的状态:state_i = 0; 对于每个指数为奇数的质因子p,state_i ^= hash[p]。 - 接着,计算前缀状态:pref[0]=0;pref[i] = pref[i-1] ^ state_{i-1} (因为a_i对应下标i-1,所以前缀状态pref[i]表示前i个数的异或状态)。 - 然后,我们需要找到两个相同的pref值,且它们的下标差最大。即对于每个值,记录它第一次出现的位置(以及后面出现的最大位置?或者只记录第一次和最后一次?)。但注意,我们要求的是最长的连续子数组,所以只需要记录每个状态第一次出现的位置,然后每次遇到相同的状态时,计算当前位置与第一次出现位置的距离,取最大值。 - 注意:初始状态pref[0]=0,位置为0(即没有元素时状态为0)。 - 遍历pref数组,同时用一个哈希表记录每个状态第一次出现的下标。如果当前状态s在之前出现过(设之前第一次出现下标为i),那么从i到当前位置j-1(因为pref[j]对应前j个元素,从0到j-1)的子数组的乘积状态为0,子数组长度为j-i。然后更新最大长度。 11. 特殊情况:整个数组的乘积为完全平方数,那么最大长度就是n。 12. 但是,如果不存在任何非空子数组满足条件,则输出-1。注意:题目要求最长子数组长度,所以如果找到了至少一个满足条件的子数组(长度至少为1),那么我们就取最大值;如果最大值至少为1,则输出最大值;如果最大值就是0(即没有找到任何满足条件的非空子数组),则输出-1。但是注意:我们记录了pref[0]=0,那么当遇到后面某个位置j使得pref[j]=0时,区间[0,j-1]的长度为j,所以一定会被计算。 13. 然而,还有一点需要注意:一个数本身如果是完全平方数,那么长度为1的子数组也是满足条件的。所以答案至少为1?不一定,因为可能整个数组中没有完全平方数,但是连续两个数的乘积可能是完全平方数(比如2和8)。所以我们需要考虑所有长度的子数组。 14. 算法步骤: - 预计算所有10^6以内的质数的随机哈希值(这里我们并不需要所有质数,只需要那些在数组a中出现的质因子,但是为了分解数组a中的数,我们需要先筛出最小质因子表,然后分解。同时为每个质数生成一个随机数,用一个数组hash_prime[]保存,大小为maxA+1,初始0。对于每个质数p,我们生成一个随机64位整数(或者128位)作为hash_prime[p]。 - 构建最小质因子数组minPrime(每个数的最小质因子),用于分解。 - 读入数组a,长度为n。 - 初始化pref[0]=0,且map(哈希表)记录状态0出现在位置0。 - 初始化max_len=0(记录满足条件的子数组的最大长度)。 - 对于i从0到n-1: 计算当前数a[i]的状态s:分解a[i](用最小质因子表快速分解),同时记录每个质因子的指数(模2),如果某个质因子指数为奇数,则s ^= hash_prime[p]。 然后pref[i+1] = pref[i] ^ s。 检查pref[i+1]是否在map中出现过: 如果出现过,设之前第一次出现的位置为j,则当前子数组长度为(i+1)-j,然后更新max_len = max(max_len, (i+1)-j)。 如果没出现过,则将当前位置i+1记录为状态pref[i+1]第一次出现的位置(注意,我们只需要记录每个状态第一次出现的位置,因为后续再遇到相同状态,用当前的位置减去第一次出现的位置会得到更长的子数组吗?实际上,我们希望得到最长的,所以只要记录第一次出现的位置,然后每次遇到相同的状态,我们计算的长度就是当前下标减去第一次出现的位置,这个长度是当前状态第二次出现到现在为止的最大可能长度?实际上,对于同一个状态,我们只需要记录它第一次出现的位置,然后后面每次出现都计算一次距离,并更新最大值即可。但是注意,我们只记录第一次出现的位置,因为这样能保证区间最长。 - 遍历结束后,如果max_len>=1,则输出max_len;否则输出-1。 15. 但是,注意:我们只记录第一次出现的位置,那么后面再出现相同状态时,用当前的位置减去第一次出现的位置,得到的是从第一次出现位置的下一个位置到当前位置的整个区间(因为前缀和是左闭右开?)。实际上,前缀和pref[i]表示前i个数的乘积状态(从0到i-1)。所以当我们在位置i+1遇到状态s,而状态s之前出现在位置j,那么区间[j, i]对应子数组a[j]到a[i]的乘积状态为0(即完全平方数),区间长度是i-j+1?不对,应该注意: pref[j] = a0^a1^...^a_{j-1} pref[i+1] = a0^a1^...^a_i pref[i+1]^pref[j] = a_j ^ ... ^ a_i,对应子数组[j, i]的乘积状态。 所以区间长度是i-j+1? 不对,下标从j到i的元素个数是i-j+1,但是我们的前缀和下标是j和i+1,所以区间长度应该是(i+1)-j(注意:pref[j]对应前j个数,pref[i+1]对应前i+1个数,所以子数组为下标j到i,个数为i-j+1,但计算长度时我们用(i+1)-j?实际上,pref[j]对应的是第0到j-1个元素,因此区间[j, i]对应的是下标j到i(共i-j+1个元素),而我们的下标j在pref数组中表示的是前j个元素,所以区间长度应该是(i+1)-j?不对,个数就是i-j+1。但是我们在计算时,位置j(pref[j])对应的是前j个元素已经处理完,那么子数组从j开始到i,所以长度为(i - j + 1)?但是我们在代码中计算的是i+1-j,因为pref[i+1]是前i+1个元素,pref[j]是前j个元素,二者异或得到从j到i(共i+1-j个元素)的乘积状态。所以区间长度就是(i+1)-j。例如,j=0(pref[0])和i+1=5(pref[5]),那么区间就是0到4,长度为5。 因此,当我们发现pref[i+1]等于pref[j]时,区间长度就是(i+1)-j。 而我们记录每个状态第一次出现的位置j,那么当我们在位置i+1遇到状态s时,区间长度就是(i+1)-j,其中j是状态s第一次出现的位置。 16. 但是,我们并不需要记录所有出现的位置,只需要记录每个状态第一次出现的位置,因为我们要最长的区间,所以只要知道最早出现的位置,那么当前位置减最早出现的位置就是当前能得到的最大长度。 17. 注意:如果数组中没有满足条件的子数组,那么max_len会保持0,此时输出-1。 18. 但是,长度为1的子数组:如果某个数a[i]本身就是完全平方数,那么它的状态s=0(因为所有质因子的指数都是偶数,所以异或后为0)。那么,在计算pref[i+1]时,pref[i+1]=pref[i]^s=pref[i]^0=pref[i]。所以我们需要检查pref[i]是否在map中出现过?出现的位置设为j,那么区间长度就是(i+1)-j。比如,当i=0时,pref[1]=pref[0]^s0,如果s0=0,那么pref[1]=0,而0已经在位置0出现过,所以区间长度=1-0=1。 19. 因此,这个算法能正确处理长度为1的子数组。 20. 性能:分解每个数的时间复杂度为O(log a_i),总时间复杂度为O(n log(max(a_i))),可以接受(因为log(max(a_i))最大约为20,n最大10^6,所以总操作次数大约2e7,在C++中可以通过)。 21. 实现细节: - 需要先线性筛出1到1000000(10^6)的最小质因子表。 - 然后为每个质数(1除外?但是1不是质数)生成随机哈希值(64位整数,用mt19937_64生成)。 - 然后读入数组,同时计算每个数的状态(分解质因数,然后指数模2,只保留指数为奇数的质因子,然后对这些质因子的哈希值求异或)。 - 然后计算前缀状态,并用unordered_map记录每个状态第一次出现的位置。 22. 注意:状态0在位置0(即pref[0]=0)已经出现,所以一开始map[0]=0。 23. 代码框架: #include <iostream> #include <vector> #include <unordered_map> #include <cstdlib> #include <ctime> #include <random> #include <algorithm> using namespace std; const int MAX = 1000000; // 10^6 vector<int> minPrime(MAX+1); // 每个数的最小质因子,0和1我们不用,或者置0 vector<long long> hash_val(MAX+1, 0); // 每个质数对应的随机哈希值,非质数位置我们不用 void sieve() { // 线性筛,并且记录最小质因子 for (int i = 2; i <= MAX; ++i) { minPrime[i] = i; // 初始化为自身 } for (int i = 2; i*i <= MAX; ++i) { if (minPrime[i] == i) { // i是质数 for (int j = i*i; j <= MAX; j += i) { if (minPrime[j] == j) { minPrime[j] = i; } } } } } void generate_hash() { // 为每个质数生成随机哈希值 mt19937_64 rng(time(0)); // 使用当前时间作为种子 for (int i = 2; i <= MAX; ++i) { if (minPrime[i] == i) { // 质数 hash_val[i] = rng(); } } } long long compute_state(int x) { // 分解x,并计算状态 long long s = 0; // 分解x while (x > 1) { int p = minPrime[x]; int cnt = 0; while (x % p == 0) { cnt++; x /= p; } if (cnt % 2 == 1) { s ^= hash_val[p]; } } return s; } int main() { sieve(); generate_hash(); int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; ++i) { cin >> a[i]; } // 前缀状态数组,我们不需要全部存储,只需要一个变量和哈希表 unordered_map<long long, int> first_occurrence; first_occurrence[0] = 0; // pref[0]=0出现在位置0 long long pref = 0; // 当前前缀状态 int max_len = 0; for (int i = 0; i < n; ++i) { // 计算a[i]的状态 long long s = compute_state(a[i]); pref = pref ^ s; if (first_occurrence.find(pref) != first_occurrence.end()) { int j = first_occurrence[pref]; // 子数组从j到i(闭区间),长度是 i - j + 1 // 注意:j是前缀状态pref第一次出现的位置,对应前j个数(即下标0到j-1)的状态,而我们当前处理的是前i+1个数(即下标0到i)的状态。 // 所以子数组对应下标j到i,长度为i-j+1? 但是按照前缀状态的定义,pref[j]对应的是前j个数(下标0到j-1),pref[i+1]对应前i+1个数(下标0到i),所以区间[j, i]对应的是从下标j到下标i(共i-j+1个数)。但是我们计算长度是用当前位置(i)和位置j,实际上,位置j+1到i+1?不对,我们直接计算区间长度:i - j + 1? // 但是注意,pref[j]对应的是前j个数(即已经处理了j个数),而我们当前处理了i+1个数,所以子数组是第j+1个数到第i+1个数?不对,下标从0开始,前j个数是a0到a_{j-1},前i+1个数是a0到a_i。那么子数组应该是从a_j到a_i(因为pref[j]和pref[i+1]异或后,把前j个数的部分抵消了,剩下a_j...a_i)。 // 所以区间长度是 i - j + 1 ? 注意:下标j到i一共有 (i - j + 1) 个数。 // 但是我们在循环中,i从0开始,j是first_occurrence[pref]的值,那么当i=0时,j=0,那么长度=0-0+1=1?正确(因为a0一个数)。 // 所以这里应该是:max_len = max(max_len, i - j + 1); // 但是注意:我们的前缀状态是在处理完a[i]之后得到的状态,所以这个状态对应的是前i+1个数。而j记录了状态pref第一次出现的位置(即当时处理了j个数),所以当前状态对应的区间是第j个数到第i个数(注意:下标从0开始,第j个数对应数组下标j,第i个数对应数组下标i-1?不对,这里需要统一) // 重新梳理: // 定义pref[i]:表示前i个数的乘积状态(即a0, a1, ..., a_{i-1})。 // 那么,我们循环中,i表示当前处理到第i个数(数组下标i),那么处理完之后,我们得到的是pref[i+1](即前i+1个数的状态)。 // 所以,当我们在第i+1个位置(即处理了i+1个数)时,状态为pref。 // 如果之前有一个位置j(表示前j个数的状态也是pref),那么从第j个数到第i个数(即a_j, a_{j+1}, ..., a_i)的乘积状态就是0(因为pref[i+1]^pref[j]=0),所以区间长度是 i - j + 1(因为数组下标j到i,共(i-j+1)个元素)。 // 但是注意:这里j是状态pref第一次出现的位置,这个位置j表示处理了j个数(即pref对应的是前j个数),那么子数组应该是从下标j到下标i(因为下标从0开始,第j个数就是a_j,第i个数就是a_i),所以长度是(i - j + 1)?不对,应该是从j到i(包含a_j到a_i)的长度是(i - j + 1)。 // 因此,这里计算长度:max_len = max(max_len, i - j + 1); 不对,因为j是前j个数,那么子数组应该是从下标j到下标i(注意:下标j对应的是第j+1个数?不对,下标0对应第一个数,下标j-1对应第j个数?) // 重新定义下标: // 设pref[0]=0,表示还没有任何数。 // 然后处理a0:状态s0,pref[1]=pref[0]^s0。 // 这时,如果pref[1]在first_occurrence中,那么第一次出现的位置是0(pref[0]=0),那么子数组就是a0(从下标0到0),长度=1。而i此时为0(循环中i=0),那么长度计算:i - 0 + 1 = 1。所以应该是:位置j(0)到位置i(0)的长度为1。 // 因此,我们实际上有:子数组的开始位置是j(在pref数组中的位置),结束位置是i(在pref数组中的位置-1)?不对,pref数组的位置j对应的子数组结束位置是j-1。所以这里我们使用: // pref数组的位置j(表示前j个数)和位置i+1(表示前i+1个数)得到子数组[j, i](即数组下标j到i,注意:j在数组a中对应下标j,i对应下标i)。 // 所以区间包含的元素个数是 i - j + 1? 不对,应该是 (i+1) - j 个元素?举个例子:j=0(pref[0])和i+1=1(pref[1]),那么区间是a0,个数为1=(1-0)=1;j=0和i+1=2,区间是a0,a1,个数为2=(2-0)=2;j=1和i+1=2,区间是a1,个数为1=(2-1)=1。所以个数就是(i+1) - j。 // 因此,区间长度 = (i+1) - j ?不对,因为j是pref数组的位置,表示前j个数,那么从j到i,个数应该是i - j + 1?但是上面的例子中,j=1和i=1(此时i+1=2),那么区间应该是a1,长度=1,即2-1=1。所以应该是:区间长度 = (i+1) - j。 // 所以这里应该是:max_len = max(max_len, i+1 - j); 但是注意,我们的循环变量i从0到n-1,那么当i=0时,j=0,区间长度=1(即1-0=1?不对,应该是1)。但是i+1=1,j=0,长度=1-0=1。 // 因此,代码: max_len = max(max_len, i+1 - j); // 因为j是pref数组的下标(从0到n),而当前pref是前i+1个数的状态,所以区间长度为(i+1)-j。 } else { // 第一次出现这个状态,记录当前位置(注意:当前处理了i+1个数,所以位置是i+1) // 但是我们在循环中,处理完a[i]后,状态是pref,这个状态对应前i+1个数,所以记录位置i+1?不对,因为我们下次遇到相同状态时,需要知道这个状态第一次出现的位置,而这个位置应该是前i+1个数之后的位置(即位置i+1)。所以我们记录first_occurrence[pref]=i+1?不对,因为我们在循环中i从0到n-1,位置从1到n。 // 但是注意,我们一开始记录pref[0]=0出现在位置0(表示前0个数)。那么当我们处理第一个元素a0后,状态pref1,那么first_occurrence[pref1]应该等于1?不对,因为pref1对应前1个数,位置为1。但是这样后面再用到这个状态时,位置1和位置i+1进行相减,长度就是(i+1)-1。 // 因此,我们记录的是前缀状态在pref数组中的位置(即下标),pref数组的下标就是处理的元素个数(0,1,...,n)。 // 所以,当前状态pref对应的是前(i+1)个数,我们在first_occurrence中记录的位置应该是i+1。 // 但是上面的计算:当遇到相同状态时,我们用当前位置(即i+1)减去第一次出现的位置j(j也是pref数组的位置),得到长度(i+1)-j。 // 所以这里应该记录:first_occurrence[pref] = i+1; // 但是注意,如果我们后面遇到相同的状态,我们希望用最早出现的位置,所以我们在第一次出现时记录,后面不再更新。 first_occurrence[pref] = i+1; } } // 注意:上面循环中,我们记录的位置实际上是pref数组的下标(即处理了多少个数),所以从0(表示0个数)到n(表示n个数)。 if (max_len == 0) { // 可能没有找到任何非空的满足条件的子数组 // 但是注意:长度为1的情况我们也考虑了,如果整个数组都没有,那么max_len=0,输出-1。 cout << -1 << endl; } else { cout << max_len << endl; } return 0; } 24. 但是上面的代码在遇到状态0时,会触发first_occurrence[0]已经存在(位置0),那么当第一次遇到状态0时(比如第一个数就是完全平方数),pref=0,那么会计算长度:i+1 - 0 = i+1。例如,i=0,长度1;i=4,长度5(整个数组)。 25. 然而,我们并没有记录状态0在位置0之后的其他位置?实际上,我们只记录了状态0第一次出现的位置(0),然后后面再出现状态0(比如整个数组乘积为完全平方数),那么我们会计算:位置i+1(比如5)减去位置0,得到5,所以整个数组长度5。因此正确。 26. 但是,如果整个数组的乘积不是完全平方数,但是中间有一段是完全平方数,也会被捕捉到。 27. 测试样例:输入5个数:1 2 3 6 1,输出5。 a[0]=1 -> state=0 -> pref[1]=0^0=0 -> 然后检查first_occurrence[0]存在(位置0)-> 长度=1-0=1 -> max_len=1。 记录状态0出现在位置1(但是注意,状态0已经在位置0出现过,所以不会记录位置1?不对,我们只在第一次出现时记录。状态0的第一次出现是在位置0,后面再出现状态0时,我们不会更新它的第一次出现位置。所以当我们处理第一个数后,状态0再次出现,我们不会记录位置1,而是直接利用第一次出现的位置0来计算长度。 a[1]=2 -> 分解2:指数为奇数,state2 = hash_val[2] -> pref[2]=pref[1]^state2=0^hash_val[2]=hash_val[2] -> 第一次出现,记录first_occurrence[hash_val[2]]=2。 a[2]=3 -> state3=hash_val[3] -> pref[3]=hash_val[2]^hash_val[3] -> 第一次出现,记录位置3。 a[3]=6 -> 分解6:2和3,指数都是1(奇数),所以state6=hash_val[2]^hash_val[3] -> pref[4]=pref[3]^state6 = (hash_val[2]^hash_val[3]) ^ (hash_val[2]^hash_val[3]) =0 -> 状态0在位置0出现过 -> 计算长度=4-0=4(这里位置4对应前4个数,即0,1,2,3?数组下标0到3,长度为4) -> max_len=max(1,4)=4。 a[4]=1 -> state=0 -> pref[5]=0^0=0 -> 状态0在位置0出现过 -> 长度=5-0=5 -> max_len=5。 所以输出5。 28. 但是,为什么中间有一个长度为4的子数组?实际上,位置0到位置3(即1,2,3,6)的乘积为1*2*3*6=36,已经是完全平方数,所以长度为4的子数组也应该被记录?但是我们最终记录的是5。所以问题不大,因为5更大。 29. 因此,代码正确。 30. 但是,注意:随机哈希值用的是64位整数,冲突概率很小,但是理论上存在冲突。如果担心冲突,可以用两个64位整数(即双哈希)或者128位整数(但C++标准库没有128位的哈希,所以我们可以用pair<long long, long long>),但是题目n=10^6,64位整数足够(冲突概率约为1e-15,可以忽略)。 31. 另外,线性筛的数组大小是MAX+1,即1000001。 32. 注意:随机数生成器不要每次都重新设置种子(在generate_hash函数中只设置一次),但是如果在main中调用一次generate_hash,那么整个程序运行期间每个质数的哈希值是固定的。 33. 但是,题目要求多次运行?只运行一次,输入一个n和n个数。 34. 最后,注意数组a[i]可能为1,分解1时,1没有质因子,所以状态为0。 35. 代码实现: 由于n最大10^6,分解每个数时,用最小质因子法分解,每次除以最小质因子,这样分解的次数是O(log a_i)的。 36. 修改:compute_state函数中,分解x: long long s = 0; while (x > 1) { int p = minPrime[x]; int cnt = 0; while (minPrime[x] == p) { cnt++; x /= p; } if (cnt % 2 == 1) { s ^= hash_val[p]; } } 37. 但是,这样写当x=1时跳出,正确。 38. 运行样例:n=5, a=[1,2,3,6,1],输出5。 39. 另一个样例:n=3, a=[2,3,6] -> 输出3?因为2*3*6=36,完全平方数。但是: a0=2: state=hash_val[2] -> pref1=0^hash_val[2] -> 记录first_occurrence[hash_val[2]]=1。 a1=3: state=hash_val[3] -> pref2=hash_val[2]^hash_val[3] -> 记录first_occurrence[hash_val[2]^hash_val[3]]=2。 a2=6: state=hash_val[2]^hash_val[3] -> pref3= (hash_val[2]^hash_val[3]) ^ (hash_val[2]^hash_val[3]) =0 -> 0在位置0出现过 -> 长度=3-0=3 -> 输出3。 40. 但是可能存在多个相同状态出现的位置,我们只记录第一次出现的位置,因为这样能保证区间长度最长。 41. 考虑另一个情况:存在多个连续1的情况?比如[1,1,1,2]: a0=1: state0=0 -> pref1=0 -> 与位置0的0匹配,得到长度1,然后不记录位置1(因为0已经出现过)。 a1=1: state1=0 -> pref2=pref1^0=0 -> 与位置0的0匹配,得到长度2(位置0到位置1:两个1)-> max_len=2。 a2=1: state2=0 -> pref3=0 -> 得到长度3(位置0到位置2:三个1)-> max_len=3。 a3=2: state3=hash_val[2] -> pref4=0^hash_val[2] -> 第一次出现,记录位置4。 所以输出3(因为3>1,2)。 42. 因此,代码正确。 43. 注意:如果整个数组没有满足条件的子数组,那么max_len保持0,输出-1。 44. 边界:n=1,且a[0]=4(完全平方数): a0=4: 分解4=2^2 -> 指数模2后为0,所以状态为0。 pref1=0,与位置0的0匹配,长度=1-0=1 -> 输出1。 45. 如果a[0]=2(不是完全平方数),那么状态为hash_val[2] -> 记录first_occurrence[hash_val[2]]=1 -> 然后后面没有相同的状态,所以max_len=0,输出-1。 46. 但是,题目要求:如果不存在,输出-1。 47. 因此,我们输出-1。 48. 代码实现: 注意:在循环中,我们计算当前数的状态,然后更新pref,然后检查pref是否在first_occurrence中,如果在,则更新max_len;否则,将pref和当前位置(i+1)加入first_occurrence(注意:只记录第一次出现)。 49. 但是,注意:状态0一开始就出现在位置0,所以如果后面再次出现0,就会计算区间长度。因此,长度为大于等于1的区间都可以被捕捉。 50. 开始写代码,注意时间效率(避免使用map,而是用unordered_map)。 51. 线性筛和生成哈希值的时间复杂度是O(MAX),可以接受(MAX=10^6)。 52. 注意:数组a[i]在1到10^6。 53. 提交代码时注意:C++标准,使用C++17。 54. 但是,随机数生成器在每次运行程序时生成的哈希值不同,但是不影响正确性(因为同一个质数每次运行程序时分配的哈希值是固定的,同一个程序运行中同一个质数的哈希值相同)。 55. 但是,如果同一个质数在多次运行中哈希值不同,但是对于同一次运行中,相同的质数哈希值相同,所以不影响。 56. 最后,考虑到题目要求,输出答案。 57. 测试一个简单样例:n=1, a[0]=1 -> 输出1。 58. 样例:n=1, a[0]=2 -> 输出-1。 59. 样例:n=2, a[0]=2, a[1]=2 -> a0: state=hash_val[2] -> pref1=hash_val[2] -> 记录first_occurrence[hash_val[2]]=1。 a1: state=hash_val[2] -> pref2=hash_val[2]^hash_val[2]=0 -> 匹配位置0(0出现过),长度=2-0=2 -> 输出2。 60. 因此,正确。 61. 代码实现如下: 注意:如果数组很大,分解质因数时注意效率。线性筛已经预处理了最小质因子,所以分解很快。 62. 但是,有一个问题:如果某个质数大于10^6?不会,因为数组元素最大10^6,所以分解得到的质因子一定在10^6以内。 63. 另外,注意1的处理:1没有质因子,所以状态为0。 64. 开始写代码。 注意:minPrime数组的预处理,对于1的处理:minPrime[1]可以设为0或者1,但我们在分解1时,x>1不成立,所以直接跳过。因此,1的状态为0。 65. 可能的问题:随机数生成器在generate_hash函数中,我们为每个质数生成了随机数。但是注意,我们只生成了质数的随机数,非质数位置是0。但是分解时,我们只用到质数位置的随机数。 66. 如果x是质数,那么minPrime[x]=x,然后我们取hash_val[x](已经生成)。 67. 如果x是合数,那么分解出的质因子p,hash_val[p]也已经生成(因为p是质数,并且小于等于x,并且小于等于10^6,所以我们在generate_hash中已经生成了)。 68. 因此,没有问题。 69. 代码: 由于时间限制,我们使用快速读写?但是题目n=10^6,cin关闭同步应该可以。 70. 提交时注意关闭同步: ios::sync_with_stdio(false); cin.tie(0); 71. 完整代码: 注意:随机数生成器使用mt19937_64,需要包含<random>。 72. 但是,随机数生成器在generate_hash中只调用一次,但是循环10^6次,所以可以提前生成随机数数组(我们已经在全局的hash_val数组中存储了每个质数的随机数)。 73. 为了避免时间开销,线性筛和生成随机数在程序开始时运行一次。 74. 注意:全局变量初始化,minPrime数组和hash_val数组。 75. 代码: 注意:在分解x的时候,x可能为1,直接返回0。 76. 修改compute_state函数: long long compute_state(int x) { if (x == 1) return 0; // 1的状态为0 long long s = 0; while (x > 1) { int p = minPrime[x]; int cnt = 0; while (minPrime[x] == p) { cnt++; x /= p; } if (cnt % 2 == 1) { s ^= hash_val[p]; } } return s; } 77. 但是,上面的循环中,x可能变为1,然后跳出循环。所以其实不需要特判1。 78. 测试x=1:while(x>1)不成立,直接返回0。 79. 因此,去掉特判。 80. 最后,注意:如果max_len>=1,则输出max_len,否则输出-1。 81. 但是,有可能存在多个状态相同的位置,但我们只记录第一次出现的位置,所以每次遇到相同的状态都会计算一次长度,并且更新max_len。所以不会漏掉。 82. 运行样例:[2,3,6,1]: a0=2: state=hash2 -> pref1=hash2 -> 记录hash2在位置1。 a1=3: state=hash3 -> pref2=hash2^hash3 -> 记录位置2。 a2=6: state=hash2^hash3 -> pref3=0 -> 匹配0(位置0),长度=3-0=3 -> max_len=3。 a3=1: state=0 -> pref4=0^0=0 -> 匹配0(位置0),长度=4-0=4 -> max_len=4。 输出4。 83. 但是,整个数组的乘积为2*3*6*1=36,完全平方数,所以长度4正确。 84. 因此,代码正确。 85. 提交代码。 86. 注意:n最大10^6,我们需要O(n)的算法,但是分解质因数最坏O(log x),所以总时间O(n log x),可以通过。 87. 但是,如果每个数都是2,那么分解很快(一次除法)。 88. 最坏情况:每个数都是不同的质数,那么每个数分解一次(一次除法),所以很快。 89. 代码: 注意:将线性筛和生成随机哈希值放在main函数之前(全局初始化)?但是这样全局变量自动初始化。但是线性筛和生成随机数我们还是写成函数,在main开头调用。 90. 但是,注意:minPrime数组需要先初始化为0,然后在线性筛中赋值。 91. 修改:在sieve函数中,我们先初始化minPrime数组(大小为MAX+1),然后令minPrime[0]=0, minPrime[1]=1?不对,1没有质因子,所以我们不分解1。在线性筛中,我们从2开始。 92. 为了保险,我们初始化minPrime数组为0,然后从2开始筛。 93. 线性筛的另一种写法(更高效): vector<int> minPrime(MAX+1, 0); vector<int> primes; void sieve() { for (int i = 2; i <= MAX; ++i) { if (minPrime[i] == 0) { minPrime[i] = i; primes.push_back(i); } for (int j = 0; j < primes.size() && primes[j] <= minPrime[i] && i*primes[j] <= MAX; ++j) { minPrime[i*primes[j]] = primes[j]; } } } 但是这里我们用最初的方法:先初始化minPrime[i]=i,然后对于质数i,将其倍数标记(如果还没有被标记,那么minPrime设为i)。这种方法也可以。 94. 我们使用最开始的方法: for (int i = 2; i <= MAX; i++) minPrime[i] = i; for (int i = 2; i*i <= MAX; i++) { if (minPrime[i] == i) { for (int j = i*i; j <= MAX; j += i) { if (minPrime[j] == j) minPrime[j] = i; } } } 95. 这样也能得到最小质因子。 96. 最后,代码: </think>以下是解决寻找乘积为完全平方数的最长子数组长度问题的C++代码实现。该问题通过质因数分解和前缀异或状态来解决,核心思路是将每个数字的质因子指数模2后的奇偶性转换为状态值,利用前缀异或和和哈希表记录状态首次出现位置,高效求解最长子数组。 ```cpp #include <iostream> #include <vector> #include <unordered_map> #include <random> #include <ctime> using namespace std; const int MAX = 1000000; // 最大值范围 vector<int> minPrime(MAX + 1); // 存储每个数的最小质因子 vector<long long> hash_val(MAX + 1, 0); // 存储每个质因子的随机哈希值 // 线性筛法预处理最小质因子 void sieve() { for (int i = 2; i <= MAX; i++) minPrime[i] = i; for (int i = 2; (long long)i * i <= MAX; i++) { if (minPrime[i] == i) { for (int j = i * i; j <= MAX; j += i) { if (minPrime[j] == j) minPrime[j] = i; } } } } // 为所有质数生成随机哈希值 void generate_hash() { mt19937_64 rng(time(0)); // 基于当前时间的随机数生成器 for (int i = 2; i <= MAX; i++) { if (minPrime[i] == i) { // i是质数 hash_val[i] = rng(); } } } // 计算数字x的状态值(质因子指数奇偶性的异或和) long long compute_state(int x) { long long s = 0; while (x > 1) { int p = minPrime[x]; int cnt = 0; while (x % p == 0) { cnt++; x /= p; } if (cnt % 2 == 1) { s ^= hash_val[p]; } } return s; } int main() { ios::sync_with_stdio(false); cin.tie(0); // 预处理最小质因子和哈希值 sieve(); generate_hash(); int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } unordered_map<long long, int> first_occurrence; first_occurrence[0] = 0; // 初始状态0在位置0 long long pref = 0; // 当前前缀异或状态 int max_len = 0; // 记录最大子数组长度 for (int i = 0; i < n; i++) { long long s = compute_state(a[i]); // 计算当前数字状态 pref ^= s; // 更新前缀状态 if (first_occurrence.find(pref) != first_occurrence.end()) { // 状态已存在,更新最大长度 int j = first_occurrence[pref]; max_len = max(max_len, i + 1 - j); } else { // 状态首次出现,记录位置 first_occurrence[pref] = i + 1; } } if (max_len == 0) { cout << -1 << endl; // 无解 } else { cout << max_len << endl; } return 0; } ``` ### 原理解释: 1. **问题分析**:完全平方数的所有质因子指数均为偶数。因此,子数组乘积为完全平方数等价于子数组中每个质因子的总指数为偶数。 2. **状态压缩**:对每个数字分解质因数,将指数模2(保留奇偶性),奇指数质因子的哈希值异或结果作为状态值。状态值为0表示所有质因子指数均为偶数(完全平方数)。 3. **前缀异或和**:定义`pref[i]`为前`i`个元素的状态异或值。子数组`[l, r]`的乘积状态等于`pref[r+1] ^ pref[l]`。当`pref[r+1] = pref[l]`时,子数组乘积为完全平方数。 4. **哈希表优化**:使用哈希表记录每个状态首次出现的位置。遍历过程中,若当前状态已存在,则计算子数组长度并更新最大值。 5. **复杂度**:预处理`O(MAX)`,分解质因数`O(log a_i)`,总时间复杂度`O(MAX + n log(max(a_i)))`,满足约束条件。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值