分块算法-例题

一、算法介绍:

  1. 首先,分块是一种线性的暴力数据结构。
  2. 其基本思想是将一段序列,分成一定数量的块,每一块有一个长度,表示一段区间。对于区间操作,通过对完整块的整体操作和对不完整块的暴力操作而使复杂度尽可能的低。当块的大小设为 sqrt(n)时间复杂度最低。
  3. 数据量在1e5之内,时间复杂度为o(n*√n)时算法可在1s内通过,时间复杂度为o(n*√n*log(n))时算法可在2s内通过。
  4. 与树状数组和线段树相比,分块简便易写,方便调试,容易理解,且具有较大的可拓展性。 

二、例题分析:

  1.分块一:https://loj.ac/problem/6277

   利用分块算法实现数据的区间修改和单点查询o(n*√n)

#include <bits/stdc++.h>

using namespace std;

const int N=5e4+5;

int n;
int sqr;
int a[N],tag[N],bel[N];    //a单个数据点,tag数据块上的加法标记,bel数据点所属分块

void Add(int l,int r,int v)
{
    for(int i=l;i<=min(r,bel[l]*sqr);i++) a[i]+=v;    //暴力处理左边不完整区间
    if(bel[r]==bel[l]) return;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++) a[i]+=v;     //暴力处理右边不完整区间
    for(int i=bel[l]+1;i<=bel[r]-1;i++) tag[i]+=v;    //完整区间处理,打标记
}

int main()
{
    cin>>n;
    sqr=(int)sqrt(n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        bel[i]=(i-1)/sqr+1;    //计算数据点属于哪个分块
    }
    for(int i=1;i<=n;i++)
    {
        int op,l,r,v;
        scanf("%d%d%d%d",&op,&l,&r,&v);
        if(op==0) Add(l,r,v);
        else
        {
            printf("%d\n",a[r]+tag[bel[r]]);
        }
    }
}

  2. 分块二:https://loj.ac/problem/6278

  区间加法 & 查询某一区间内小于值x的数据个o(n*√n*log(n))

#include <bits/stdc++.h>

using namespace std;

const int N=5e4+5;

int n;
int sqr;
int a[N],bel[N],tag[N];
vector<int>vis[505];    //利用动态数组储存分块数据,用lower_bound()进行二分查询

void Reset(int l)    //不完整区间修改tag标记无效,需要对区间中所有数进行重构和排序
{
    int j=bel[l];
    vis[j].clear();
    for(int i=(j-1)*sqr+1;i<=min(n,j*sqr);i++)
    {
        vis[j].push_back(a[i]);
    }
    sort(vis[j].begin(), vis[j].end());
}

void Add(int l,int r,int v)
{
    for(int i=l; i<=min(r,bel[l]*sqr); i++) a[i]+=v;    //不完整区间处理
    Reset(l);
    if(bel[l]==bel[r]) return;
    for(int i=(bel[r]-1)*sqr+1; i<=r; i++) a[i]+=v;
    Reset(r);


    for(int i=bel[l]+1;i<=bel[r]-1;i++)    //完整区间处理
    {
        tag[i]+=v;
    }
}

int Query(int l,int r,int v)
{
    int re=0;
    for(int i=l;i<=min(r,bel[l]*sqr);i++)
    {
        if(a[i]+tag[bel[l]]<v) re++;
    }
    if(bel[l]==bel[r]) return re;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++)
    {
        if(a[i]+tag[bel[r]]<v) re++;
    }
    for(int i=bel[l]+1;i<=bel[r]-1;i++)
    {
        int tmp=lower_bound(vis[i].begin(),vis[i].end(),v-tag[i])-vis[i].begin();    //log(n)的来历
        re+=tmp;
    }
    return re;
}

int main()
{
    cin>>n;
    sqr=(int)sqrt(n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i]);
        bel[i]=(i-1)/sqr+1;
        vis[bel[i]].push_back(a[i]);    //储存分块数据
    }
    for(int i=1; i<=(n-1)/sqr+1; i++)
    {
        sort(vis[i].begin(), vis[i].end());    //将每一个分块数据升序排列
    }
    int op,l,r,v;
    for(int i=1; i<=n; i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&v);
        if(op==0) Add(l,r,v);
        else
        {
            printf("%d\n",Query(l,r,v*v));
        }
    }

}

  3. 分块三:https://loj.ac/problem/6279

  区间加法 & 询问某区间内小于x的最大值

#include <bits/stdc++.h>

using namespace std;

const int N=1e5+5;

int n;
int sqr;
int a[N],bel[N],tag[N];
vector<int>vis[505];

void Reset(int l)
{
    int j=bel[l];
    vis[j].clear();
    for(int i=(j-1)*sqr+1;i<=min(n,j*sqr);i++)
    {
        vis[j].push_back(a[i]);
    }
    sort(vis[j].begin(), vis[j].end());
}

void Add(int l,int r,int v)
{
    for(int i=l;i<=min(r,bel[l]*sqr);i++) a[i]+=v;
    Reset(l);
    if(bel[l]==bel[r]) return;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++) a[i]+=v;
    Reset(r);
    for(int i=bel[l]+1;i<=bel[r]-1;i++)
    {
        tag[i]+=v;
    }
}

int Query(int l,int r,int v)        //与分块二不同的地方
{
    int ans=-0x3f3f3f3f;
    for(int i=l;i<=min(r,bel[l]*sqr);i++)
        if(a[i]+tag[bel[l]]<v)
        ans=max(ans,a[i]+tag[bel[l]]);
    if(bel[l]==bel[r]) return ans;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++)
        if(a[i]+tag[bel[r]]<v)
        ans=max(ans,a[i]+tag[bel[r]]);
    for(int i=bel[l]+1;i<=bel[r]-1;i++)
    {
        int tmp=lower_bound(vis[i].begin(),vis[i].end(),v-tag[i])-vis[i].begin();
        if(tmp==0) continue;
        else
        {
            ans=max(ans,vis[i][tmp-1]+tag[i]);
        }
    }
    return ans;
}

int main()
{
    scanf("%d",&n);
    sqr=(int)sqrt(n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        bel[i]=(i-1)/sqr+1;
        vis[bel[i]].push_back(a[i]);
    }
    for(int i=1;i<=(n-1)/sqr+1;i++)
    {
        sort(vis[i].begin(), vis[i].end());
    }
    int op,l,r,v;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&v);
        if(op==0) Add(l,r,v);
        else
        {
            int ans=Query(l,r,v);
            if(ans==-0x3f3f3f3f) printf("%d\n",-1);
            else printf("%d\n",ans);
        }
    }
}

  4. 分块四:https://loj.ac/problem/6280

  区间加法 & 区间求和

#include <bits/stdc++.h>

using namespace std;

const int N=1e5+5;

typedef long long LL;

int n,sqr;
LL a[N],tag[N],sum[N];    //sum用于储存块内数据和,方便查询
int bel[N];

//左右不完整区间更新a[i],更新sum; 完整区间更新tag

void Add(int l,int r,int v)
{
    for(int i=l;i<=min(r,bel[l]*sqr);i++)
    {
        a[i]+=v;
        sum[bel[l]]+=v;
    }
    if(bel[l]==bel[r]) return;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++)
    {
        a[i]+=v;
        sum[bel[r]]+=v;
    }
    for(int i=bel[l]+1;i<=bel[r]-1;i++)
    {
        tag[i]+=v;
    }
}
LL Query(int l,int r)
{
    LL ans=0;
    for(int i=l;i<=min(r,bel[l]*sqr);i++) ans+=a[i]+tag[bel[l]];
    if(bel[l]==bel[r]) return ans;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++) ans+=a[i]+tag[bel[r]];
    for(int i=bel[l]+1;i<=bel[r]-1;i++) ans+=tag[i]*sqr+sum[i];
    return ans;
}
int main()
{
    cin>>n;
    sqr=(int)sqrt(n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        bel[i]=(i-1)/sqr+1;
        sum[bel[i]]+=a[i];
    }
    int op,l,r,v;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&v);
        if(op==0) Add(l,r,v);
        else
        {
            printf("%d\n",Query(l,r)%(v+1));
        }
    }
}

  5. 分块五:https://loj.ac/problem/6281

  对某一区间所有数进行开方 & 区间和查询

#include <bits/stdc++.h>

using namespace std;

const int N=5e4+5;

typedef long long LL;

int n,sqr;
LL a[N],sum[N];    //sum用于储存数据块内值的和
int bel[N];
bool avl[N];       //判断某一数据块是否还有必要进行开方处理(0,1)

void Update(int l,int r)
{

    if(avl[bel[l]])                         //暴力左右不完整区间
    for(int i=l;i<=min(r,bel[l]*sqr);i++)
    {
        sum[bel[l]]-=a[i];
        a[i]=sqrt(a[i]);
        sum[bel[l]]+=a[i];
    }
    if(bel[l]==bel[r]) return;
    if(avl[bel[r]])
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++)
    {
        sum[bel[r]]-=a[i];
        a[i]=sqrt(a[i]);
        sum[bel[r]]+=a[i];
    }



    for(int i=bel[l]+1;i<=bel[r]-1;i++)      //对完整区间进行数据块重构
    {
        if(avl[i])
        {
            avl[i]=false;
            sum[i]=0;
            for(int j=(i-1)*sqr+1;j<=min(n,i*sqr);j++)
            {
                a[j]=sqrt(a[j]);
                sum[i]+=a[j];
                if(a[j]>1) avl[i]=true;
            }
        }
    }
}

LL Query(int l,int r)
{
    LL re=0;
    for(int i=l;i<=min(r,bel[l]*sqr);i++) re+=a[i];
    if(bel[l]==bel[r]) return re;
    for(int i=(bel[r]-1)*sqr+1;i<=r;i++) re+=a[i];
    for(int i=bel[l]+1;i<=bel[r]-1;i++) re+=sum[i];
    return re;
}

int main()
{
    cin>>n;
    sqr=(int)sqrt(n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        bel[i]=(i-1)/sqr+1;
        sum[bel[i]]+=a[i];
        if(a[i]>1) avl[bel[i]]=true;
    }
    int op,l,r,v;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&v);
        if(op==0) Update(l,r);
        else
        {
            printf("%lld\n",Query(l,r));
        }
    }
}

 

<think>我们正在讨论C++中的分治算法例题目和代码实现。根据引用内容,我们可以参考LeetCode上的几个经典分治问题。以下是三个常见的例题及其C++实现:1.**Leetcode50:Pow(x,n)**-实现幂函数2.**Leetcode169:多数元素**-寻找数组中出现次数超过一半的元素3.**Leetcode53:最大子序和**-求连续子数组的最大和下面分别给出这三个问题的分治解法代码实现。###1.Leetcode50:Pow(x,n)题目要求实现计算$x$的$n$次幂的函数,即$pow(x,n)$。使用分治法可以降低时间复杂度到$O(\logn)$。**分治思路**:-将$n$分解为两半,计算$pow(x,n/2)$,然后根据$n$的奇偶性合并结果。-注意处理$n$为负数的情况。```cppclassSolution{public:doublemyPow(doublex,intn){longlongN=n;//防止n为负数时取相反数溢出if(N<0){x=1/x;N=-N;}returnfastPow(x,N);}doublefastPow(doublex,longlongn){if(n==0){return1.0;}doublehalf=fastPow(x,n/2);if(n%2==0){returnhalf*half;}else{returnhalf*half*x;}}};```###2.Leetcode169:多数元素题目要求找出数组中出现的次数大于$\lfloorn/2\rfloor$的元素。**分治思路**:-将数组分成左右两部分,分别求出左半部分的众数和右半部分的众数。-如果左右两部分的众数相同,则直接返回。-如果不同,则在整个数组中统计这两个数出现的次数,返回出现次数多的那个。```cppclassSolution{public:intmajorityElement(vector<int>&nums){returnmajorityElementRec(nums,0,nums.size()-1);}intmajorityElementRec(vector<int>&nums,intleft,intright){//递归终止条件:当子数组只有一个元素时,该元素就是众数if(left==right){returnnums[left];}//分治:将数组分成左右两部分intmid=left+(right-left)/2;intleftMajority=majorityElementRec(nums,left,mid);intrightMajority=majorityElementRec(nums,mid+1,right);//如果左右两部分的众数相同,则直接返回if(leftMajority==rightMajority){returnleftMajority;}//否则,统计左右众数在整个子数组中的出现次数intleftCount=countInRange(nums,left,right,leftMajority);intrightCount=countInRange(nums,left,right,rightMajority);returnleftCount>rightCount?leftMajority:rightMajority;}intcountInRange(vector<int>&nums,intleft,intright,inttarget){intcount=0;for(inti=left;i<=right;i++){if(nums[i]==target){count++;}}returncount;}};```###3.Leetcode53:最大子序和题目要求计算一个整数数组中的连续子数组的最大和。**分治思路**:-将数组分成左右两部分,最大子序和可能出现在左半部分、右半部分或跨越左右两部分。-分别递归求出左半部分和右半部分的最大子序和。-再计算跨越中点的最大子序和(从中点向左扩展和向右扩展,取最大和)。-最后取三者中的最大值。```cppclassSolution{public:intmaxSubArray(vector<int>&nums){returnmaxSubArrayHelper(nums,0,nums.size()-1);}intmaxSubArrayHelper(vector<int>&nums,intleft,intright){if(left==right){returnnums[left];}intmid=left+(right-left)/2;//分别求左右子数组的最大子序和intleftSum=maxSubArrayHelper(nums,left,mid);intrightSum=maxSubArrayHelper(nums,mid+1,right);//求跨越中点的最大子序和intcrossSum=crossSubArray(nums,left,right,mid);//返回三者中的最大值returnmax(max(leftSum,rightSum),crossSum);}intcrossSubArray(vector<int>&nums,intleft,intright,intmid){//从中点向左扩展,求最大和intleftSum=INT_MIN;intsum=0;for(inti=mid;i>=left;i--){sum+=nums[i];leftSum=max(leftSum,sum);}//从中点向右扩展,求最大和intrightSum=INT_MIN;sum=0;for(inti=mid+1;i<=right;i++){sum+=nums[i];rightSum=max(rightSum,sum);}//合并左右两边的和returnleftSum+rightSum;}};```###总结以上三个例题展示了分治算法的典型应用:将问题分解为规模更小的子问题,递归求解,然后合并结果。分治法通常能降低问题的时间复杂度(如从$O(n)$到$O(\logn)$),但需要注意递归的终止条件以及合并结果的逻辑。§§相关问题§§1.分治算法和动态规划有什么区别?2.除了以上例题,还有哪些经典问题可以用分治算法解决?3.分治算法的时间复杂度分析通常使用什么方法?</think>### C++ 分治算法例题目及代码实现 分治算法通过**分解→解决→合并**三步解决复杂问题,以下是三个经典例题的C++实现: --- #### 1. **Leetcode 50: Pow(x, n)** [^1] **问题**:实现计算 $x^n$ 的函数。 **分治思路**: - 分解:$x^n = x^{n/2} \times x^{n/2}$(若 $n$ 为偶数) - 合并:根据 $n$ 的奇偶性调整结果 ```cpp class Solution { public: double myPow(double x, int n) { long long N = n; // 防止n为负数时溢出 if (N < 0) { x = 1 / x; N = -N; } return fastPow(x, N); } double fastPow(double x, long long n) { if (n == 0) return 1.0; double half = fastPow(x, n / 2); return (n % 2 == 0) ? half * half : half * half * x; } }; ``` **时间复杂度**:$O(\log n)$ --- #### 2. **Leetcode 169: 多数元素** [^1] **问题**:找出数组中出现次数超过 $\lfloor n/2 \rfloor$ 的元素。 **分治思路**: - 分解:将数组分为左右两半 - 解决:递归求左右子数组的众数 - 合并:统计两个众数在整个数组的频率,取更高者 ```cpp class Solution { public: int majorityElement(vector<int>& nums) { return divide(nums, 0, nums.size()-1); } int divide(vector<int>& nums, int l, int r) { if (l == r) return nums[l]; int mid = l + (r - l) / 2; int leftMajor = divide(nums, l, mid); int rightMajor = divide(nums, mid+1, r); // 合并:统计左右众数的全局频率 if (leftMajor == rightMajor) return leftMajor; int leftCount = count(nums, l, r, leftMajor); int rightCount = count(nums, l, r, rightMajor); return leftCount > rightCount ? leftMajor : rightMajor; } int count(vector<int>& nums, int l, int r, int target) { int cnt = 0; for (int i = l; i <= r; ++i) { if (nums[i] == target) cnt++; } return cnt; } }; ``` **时间复杂度**:$O(n \log n)$ --- #### 3. **Leetcode 53: 最大子序和** [^1] **问题**:计算连续子数组的最大和。 **分治思路**: - 分解:将数组分为左右两半 - 解决:递归求左半、右半的最大和 - 合并:计算跨越中点的最大和(关键步骤) ```cpp class Solution { public: int maxSubArray(vector<int>& nums) { return maxSub(nums, 0, nums.size()-1); } int maxSub(vector<int>& nums, int l, int r) { if (l == r) return nums[l]; int mid = l + (r - l) / 2; int leftSum = maxSub(nums, l, mid); // 左半最大和 int rightSum = maxSub(nums, mid+1, r); // 右半最大和 int crossSum = maxCrossing(nums, l, r); // 跨越中点的最大和 return max({leftSum, rightSum, crossSum}); } int maxCrossing(vector<int>& nums, int l, int r) { int mid = l + (r - l) / 2; // 向左扩展的最大和 int leftSum = INT_MIN, sum = 0; for (int i = mid; i >= l; --i) { sum += nums[i]; leftSum = max(leftSum, sum); } // 向右扩展的最大和 int rightSum = INT_MIN; sum = 0; for (int i = mid+1; i <= r; ++i) { sum += nums[i]; rightSum = max(rightSum, sum); } return leftSum + rightSum; } }; ``` **时间复杂度**:$O(n \log n)$ --- ### 分治算法核心要点总结 1. **适用场景**:问题可分解为**相互独立**的子问题(如数组分块、树形结构) 2. **关键步骤**: - **分解**:将原问题划分为规模相同的子问题 - **解决**:递归求解子问题(递归终止条件需明确定义) - **合并**:将子问题的解组合成原问题的解 3. **效率优势**:通过降低问题规模(如二分)将 $O(n)$ 优化至 $O(\log n)$ > 引用说明:示例题目均来自LeetCode经典分治问题[^1],代码实现采用标准分治模板[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值