这篇文章上次修改于 447 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

算法学习笔记(7): 根号分治

根号分治是一种对暴力的优化,类似于一种优雅的暴力。

对于一个问题的解决,我们可能有多种暴力的思路,但对于暴力思路A来说,空间占用过大。对于暴力思路B来说,时间占用过大。

两个暴力思路各有自己的优点。这里举出题目例子更好理解。

P3396 哈希冲突

题目大意:给出一个数组arr,有多次查询,每次询问有两个值,m和k,问下标模m余k的所有元素之和。

对于这个题目要求,有两个暴力思路

思路A

如果只有一次查询,我们的暴力思路就是

int result = 0;
for(int i = k; i < n; i += m)
    result += arr[i];

但实际有多次查询,这种查询十分消耗时间,m越小,需要添加的次数就越多

为了减少这种多次查询的高时间复杂度,可以采用空间换时间的思路

思路B

使用一个二维数组保存查询结果

cin >> a[i];
for(int m = 1; m< n; m++)
    res[m][i % m] += a[i];

其中res[m][k]就代表了以m为除数,余数为k的数的总和。

但是我们可以发现,res的大小是n * n,一旦n比较大,将会使用大量的空间,并且有一半的空间都属于空的没有用的状态,消耗了很多空间。但查询成功做到了O(1)

分析

对两个思路进行分析,分析其优缺点。

思路A可以对大的m进行高效查找并且节约空间

思路B可以对小的m进行高效缓存并且节约时间

为此,不妨进行一个结合,如果找到一个分界点,让小的m使用思路B,让大的m使用思路A,就可以兼得两者的优点,达到空间和时间的平衡,对于这个思路,有一个结论就是,分界点取根号N的常数倍。

这个思路就是根号分治

题解代码

#include <bits/stdc++.h>

using namespace std;

int ans[501][501];

int main()
{
    int n, m;
    cin >> n >> m;

    vector<int> arr(n + 1);
    // vector<vector<int>> ans(n + 2, vector<int>(n + 2));

    int line = sqrt(n);

    for (int i = 1; i <= n; i++)
    {
        cin >> arr[i];
        for (int p = 1; p <= line; p++)
            ans[p][i % p] += arr[i];
    }
    while (m--)
    {
        int x, y;
        char op;
        cin >> op >> x >> y;
        if ('A' == op)
        {
            if (x <= line)
                cout << ans[x][y] << endl;
            else
            {
                int sum = 0;
                for (int i = y; i <= n; i += x)
                    sum += arr[i];
                cout << sum << endl;
            }
        }
        else
        {
            for (int p = 1; p <= line; p++)
            {
                ans[p][x % p] -= (arr[x] - y);
            }
            arr[x] = y;
        }
    }
}

这种算法的时间复杂度是O(N根号N)

根号分治适用于这种多查询的问题,其中对于查询来说,还有密度问题,前密后疏适合使用

对于分界点,分界点越小,时间越大空间越小。分界点越大,时间越小空间越大

练习可见codeforces920 F题,题型一致