Square-free division
原题传送门
题目大意:
给你一个数列,要你将其划分成若干个连续的区间,使得在每个区间中,任意两个数的乘积不是完全平方数,求满足这个条件的所有划分方法中,区间个数的最小值。
知识点分析:
1.欧拉筛的应用
2.算数基本定理
3.stl的应用(map)
那么我们一步一步的在分析求解这道题目。
1.首先我们需要分析任意两个数的乘积不为完全平方数的条件,只有知道这个,我们才能获得划分区间的依据,才能将题目继续做下去。
考虑使用算术基本定理先将数分解成质数形式,我们从数列中任取两个数分别为 a i , a j a_i,a_j ai,aj。
然后将这两个数都分解成质数的形式。‘
a
i
=
p
b
1
a
1
∗
p
b
2
a
2
.
.
.
p
b
k
a
k
\displaystyle a_i=p_{b_1}^{a_1}*p_{b_2}^{a_2}...p_{b_k}^{a_k}
ai=pb1a1∗pb2a2...pbkak
a
j
=
p
c
1
d
1
∗
p
c
2
d
2
.
.
.
p
c
m
d
m
\displaystyle a_j=p_{c_1}^{d_1}*p_{c_2}^{d_2}...p_{c_m}^{d_m}
aj=pc1d1∗pc2d2...pcmdm
然后我们接着分析,一个数是完全平方数,那么它的质数分解后的形式中,每个质数的指数一定是偶数,否则,它将不是完全平方数,因为它不能为整体贡献一个平方出来,即二次方。
那么对于
a
i
,
a
j
\displaystyle a_i,a_j
ai,aj,我们只要记录它们质因子,然后将它们两个的质因子乘起来,判断每个质因子的指数是否存在奇数即可,但是如果直接这样做,貌似有点复杂,复杂度也会上升,我们需要更进一步的分析每个质数及其指数的贡献。我们知道,偶数次幂的质数,都可以写成完全平方的形式,也就是说,它对整体的贡献可以忽略,思考一下,如果我们要判断一个数是否可以写成完全平方数,只需要看它分解后的指数中是否存在奇数,而奇数这个性质,不论它到底有多大,它是奇数,那么在奇偶性质上等价于1,然后不论是做加减法,还是乘法,它的性质变化都与1相同。考虑到这个后,数列中的每一项对一个乘积的贡献都等价于它的质因子中,指数是奇数的质因子的乘积。偶数次都可以忽略对吧。
2.对于这样两个化简后的表达式,这里记为mask[i],mask[j]。如果它们的乘积是完全平方式,那么它们的mask一定相等。
即:
m
a
s
k
[
i
]
=
=
m
a
s
k
[
j
]
mask[i]==mask[j]
mask[i]==mask[j]
否则,一定存在一个质因子,它的指数不为偶数,那么这两个数就不能构成完全平方数。
3.然后我们就可以贪心的去做这件事情,我们要让这个区间个数尽可能的小,我们就从第一个数开始去取区间,尽可能的维护当前的区间,让其最长即可。
4.但是这里有个小细节要注意,我们在判断当前区间最长值得时候,是要找到当前轮中重复出现得那个数,一旦出现之后,就要刷新这个信息,但这样会大大增加我们程序得运行效率,但是没有关系,我们可以用L来表示当前轮,每次遇到重复得就更新这个标记,同时不断更新map得值来表示某个数最近出现是在哪一轮。
5.但是由于数据量较大,我们不能够直接对所有数进行质因数分解,而官方给出得方法真的是太巧妙了。下面我们着重分析这个变形得欧拉筛。
首先我们要分解每个数,我们需要预处理出在1e7范围内的每个质数,这样在分解的时候才更方便,不用重复去找,更何况这还是多组数据,但是我们在想一想,是不是还可以进一步优化呢。我们知道,我们使用欧拉筛是因为它更高效,它保证了每个数只被筛一次(即用每个数的最小质因子去筛),也就是说,我们在筛的过程中就能获取每个数的最小质因子,我们只需要保存这个信息,这样,我们就可以避开枚举,直接找到每个数的最小质因子,然后除去,得到一个新的数,重复上述过程即可,复杂度大大降低。
具体看代码,有注释。
//#include<bits/stdc++.h> ----万能头----
#include <iostream>
#include<stdio.h>
#include<algorithm>
#include<string.h>
#include<cstring>
#include<cmath>
#include<queue>
#include<list>
#include<map>
#include<unordered_map>
using namespace std;
const int p = 1e9+7;
const int N = 1e7+5;
const int maxn = 1e5+10;
const long long INF = 1<<31;
#define REP(i,n) for(int i = 1; i <= n; ++i)
#define REPA(i,j,n) for(int i = j; i <= n; ++i)
#define RREP(i,n) for(int i = n; i >= 1; --i)
#define REPR(i,j,n) for(int i = n; i >= j; --i)
typedef long long ll;
typedef pair<ll,ll> PII;
ll n,m,t;
ll k;
ll a[maxn];
ll mask[maxn];
ll mind[N];
vector<int> q;//存放质数
void get_pr() //欧拉筛变形,记录最小质因数
{
for(int i = 2;i <= 1e7; ++i)
{
if(mind[i] == 0)//这里跟欧拉筛一样,如果当前位没有被筛过,则为质数
{
mind[i] = i;//质数的最小质因子为其本身
q.emplace_back(i);//这样貌似效率更高相对于push
}
for(auto &pr : q)//枚举之前获得的质数
{
if(pr*i > 1e7 || pr > mind[i])break;//这里跟欧拉筛稍稍有些不一样,但本质上完全一致,k*i防止越界
// 本来在欧拉筛中是判断当前数中如果包含了当前枚举的质数后,就要停止,防止重复筛,而这里由于我们记录的是每个数的最小质因数。
// 所以我们只要判断当前的质数是否大于当前数的最小质因数
mind[pr*i] = pr;
}
}
}
void solve() {
cin>>n>>k;
for(int i = 1;i <= n; ++i)scanf("%lld",&a[i]);
for(int i = 1;i <= n; ++i){
ll cnt = 0, x = a[i], last = 0;
mask[i] = 1;//init
while(x > 1){ //如果现在剩下的数仍然大于1,则意味着,当前数还可以继续被质数分解
ll p = mind[x];//获取最小质因子
if(p == last){//如果当前数的最小质因子与刚除掉的质因子相同,即意味着,最小质因子的指数不为1,则记录指数。
//由于最小质因子一定是随着x的减小而单调递增的,由算术基本定理可以很好理解
++cnt;
}
else{//这里就意味着如果当前的最小质因子与上一次除去的不同,我们就要计算上一个质因子对mask的贡献
if(cnt&1)//如果出现了奇数次,则要计算贡献
mask[i]*=last;
last = p;//更新指数
cnt = 1;
}
x/=p;
}
if(cnt & 1)mask[i]*=last;//不要忘记如果最后一个质因子出现了奇数次,他的贡献可不能遗忘
}
ll ans = 1,L = 0;
map<ll,ll> mp;
for(int i = 1;i <= n; ++i){
if(mp.find(mask[i]) != mp.end() && mp[mask[i]] >= L){
++ans;
L = i;//用来更新当前在哪个区间
}
mp[mask[i]] = i;//不断更新当前出现的mask的值所对应的区间
}
cout<<ans<<'\n';
}
int main() {
//ios::sync_with_stdio(0);
//cin.tie(0);
//cout.tie(0);
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
get_pr();
scanf("%lld",&t);
while(t--)
solve();
fclose(stdin);
fclose(stdout);
}