原题见HDU 5726
给N( N≤100000 )个数,Q( Q≤100000 )个询问,每次查询输出区间的最大公约数,以及最大公约数为这个数的区间数目。
分析
查询次数很多,要做预处理,用map<最大公约数,区间数>存下来,实现O(1)的查询。
预处理发现对于同一左端点的区间而言,右端点越靠右,区间gcd单调递减。因此可以固定左端点,二分右端点,找到gcd突变的右端点,确定对于同一gcd的区间数目。为了查询得更快,用ST表(RMQ)存下区间gcd,只要O(1)即可查询得到区间gcd。
操作
RMQ建表
dp[i][j]
表示长度为
2i
的区间
[i,i+2j−1]
范围内的
gcd
即区间 [i,i+2j−1−1]与[i+2j−1,i+2j−1] 共同确定了区间 [i,i+2j−1] 的 gcd
for(int i = 0;i < n;i++){
dp[i][0] = a[i];
}
for(int j = 1;(1<<j) <= n;j++){
for(int i = 0;i + (1<<j) - 1 < n;i++){
dp[i][j] = __gcd(dp[i][j-1], dp[i+(1<<j-1)][j-1]);
}
}
RMQ询问
[l,r] 由 [l,...]与[...,r] 共同决定。两者必须共同覆盖了整个区间。区间长度为 2k . k=log2(r−l+1)
int ask(int l, int r){
int k = log(1.0*(r-l+1))/log(2);
return __gcd(dp[l][k], dp[r-(1<<k)+1][k]);
}
据MrBird_to_fly回忆,还有一种对 log2(r−l+1) 的优化,就有点邪了,非常省时。
int k=31-__builtin_clz(r-l+1);
int __builtin_ctz (unsigned int x)
返回右起第一个‘1’之后的0的个数。
统计预处理
map<int, int> mp;
int find(int x, int g){
int l = x, r = n-1;
if(ask(x, r) > g) return r+1;
if(ask(x, l) <= g) return l;
int cnt = 20;
while(l < r && cnt--){
int mid =l+r >> 1;
if(ask(x, mid) > g) l = mid;
else r = mid;
}
if(ask(x, l) > g) return r;
return l;
}
void makeST(){
mp.clear();
for(int i = 0;i < n;i++){
for(int j = i;j < n;){
int g = ask(i, j);
int k = find(i, g-1);
mp[g] += k-j;
j = k;
}
}
}
优化
上面的预处理左端点逐个枚举,其实重复查询了很多个转折点。预处理复杂度为O(nlog(a)log(a))(a<=10^9)
现在先固定区间右端点,左端点从下标0处开始向右,直到与右端点重合,这过程中gcd逐渐增大,并且也存在转折点。
右端点右移,则可以利用上一个右端点的转折点得到现在的转折点。上一次的转折点要么被舍去(两边的gcd值相等),要么继续保留,但不会再增加(除了右端点作为转折点)。
举例:10 20 3 15 1000 60 16
右端点 | 转折点(下标) | gcd |
---|---|---|
10 | 10(0) | 10 |
20 | 10(0), 20(1) | 10,20 |
3 | 10(0), 3(2) | 1,3 |
15 | 10(0), 3(2), 15(3) | 1,3,15 |
1000 | 10(0), 15(3), 1000(4) | 1,5,1000 |
60 | 10(0), 15(3), 1000(4), 60(5) | 1,15,20,60 |
16 | 10(0), 60(5), 16(6) | 1,4,16 |
每次都以右端点和上一次的转折点对应的gcd得到gcd,只有得到不同gcd才将该转折点保留下来,否则舍去。比如右端点为3时,10,20和3的最大公约数都为1,只需保留10.
需注意的是,右端点和上一次转折点的gcd并不是右边这一栏的gcd。如右端点为15时,转折点为10,为什么对应的gcd为1呢?这是由于1是[10,20,3]的gcd,所以[10,20,3,15]的gcd只能是1的约数。
优化后的复杂度为O(nlog(a))
typedef map <int, LL> MAP;
typedef pair<int, int> PR;
typedef vector<PR> DV;
DV dv, tp;
MAP mp;
void makeTable{
dv.clear();
for(int i = 0;i < n;i++){
int g = -1;
tp.clear();
dv.push_back(PR(a[i], i));
for(DV::iterator it = dv.begin();it != dv.end();it++){
if(__gcd(it->first, a[i]) != g){
g = __gcd(it->first, a[i]);
tp.push_back(PR(g, it->second));
}
}
tp.push_back(PR(0, i+1));
for(DV::iterator it = tp.begin();it+1 != tp.end();it++)
mp[it->first] += (it+1)->second-it->second;
swap(dv, tp);
}
}
应用
CF 475D. CGCDSSQ
求最大公约数为g的区间数。
数据范围和上面那题差不多,但是时间只有2s,不优化就会T(也许是写得搓)。
预处理优化 142ms
log优化 436ms