尺取法(双指针)及例题

方法引入

有一组数,现在给定一个数k,问这组数中两个数加和为k的情况有多少?

  • 考虑暴力,显然可以使用两个for循环,时间复杂度为O(n2),对于n在104以上的数据无法在规定时间内得到答案,需要寻找更好的方法
  • 因为顺序不影响答案,所以考虑先排个序,要的是两个数加和,所以需要两个指针,分别指向这两个数,可以把两个指针分别指向数组头和尾,如果两个数加和比k大,那么显然需要将尾指针向前移动一位;如果比k小,显然需要将头指针向后移动一位;相等的时候即为答案
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 2e5+100;
int Data[MAXN];
int main(){
    int n,sum;
    cin>>n>>sum;
    for(int i=1;i<=n;i++) scanf("%d",Data + i);
    int i = 1,j = n;
    int ans = 0;
    while(i < j){
        if(Data[i] + Data[j] < sum){
            j++;
        }
        else if(Data[i] + Data[j] > sum){
            i++;
        }
        else{
            ans++;
            i++;
            j--;
        }
    }
    return 0;
}

方法总结

  • 这就是一种尺取法的表现方式,尺取法不一定是非要从前往后,也有可能是两边到中间,具体问题需要具体分析,还要注意问题能否使用尺取法,一般区间上的问题就可能要使用尺取法

具体问题

模板题

poj3061

  • 给定一个数s,求一个连续子序列,让它的和大于等于s,问有多少个这样的连续子序列
  • 显然问题和数组元素位置是有关系的,那么可以考虑从前往后推导,开始时候双指针位置都在第一个,如果两指针区间和小于s,那么右指针右移;否则区间和更新,左指针右移,直至r到达n
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 2e5+100;
int Data[MAXN];
int main(){
    int t, s, n;
    cin>>t;
    while(t--){
        scanf("%d%d",&n, &s);
        for(int i=0;i<n;i++) scanf("%d", Data + i);
        int l, r;
        l = r = 0;
        int sum = 0;
        int ans = INF;
        while(r <= n){
            if(sum < s){
                sum += Data[r];
                r++;
            }else{
                ans = min(ans, r - l);
                sum -= Data[l];
                l++;
            }
        }
        ans == INF?printf("0\n"):printf("%d\n", ans);
    }
    return 0;
}

最大子段和

最大子段和
求一组数的最大子段和

  • 这其实是一个动态规划的问题,子段可以从前往后dp进行,如何选择当前元素取决于该元素的前缀和和它本身之间的大小关系,如果前缀和比它大,那么更新sum为前缀和;否则更新sum为当前元素
  • 有状态转移方程dp[i] = max(dp[i - 1] + Data[i], Data[i]),程序可使用尺取法思想编写也就是右指针向右移动,不停更新sum(sum表示前缀和),这里面判断条件为sum + Data[i] < Data[i]化简即为sum < 0
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 2e5+100;
int Data[MAXN];
int main(){
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++) scanf("%d",Data + i);
    int sum = 0, ans = -INF;
    for(int i=0;i<n;i++){
        if(sum < 0) sum = Data[i];
        else sum += Data[i];
        if(sum > ans){
            ans = sum;
        }
    }
    cout<<ans;
    return 0;
}

题目链接

  • 给出一个环形序列,求序列中最大连续子序列
  • 分两种情况考虑,其一是去环的最大连续子序列;其二是带环的最大连续子序列,第一种情况就是最大子段和;第二种情况可以考虑正难则反,找到最小子段和,用数列和减去它即为答案,最后取大值
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 2e6+100;
int Data[MAXN];
int main(){
    int t,n;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
    int ans1 = -INF,ans2 = INF;
        int r = 0;
    int sum = 0;
    int tot = 0;
        for(int i=0;i<n;i++){
            scanf("%d",Data + i);
            tot += Data[i];
        }
        while(r < n){
            if(sum + Data[r] < 0) sum = 0;
            else{
                sum += Data[r];
                ans1 = max(ans1, sum);
            }
            r++;
        }
        sum = 0;
        r = 0;
        while(r < n){
            if(sum + Data[r] > 0) sum = 0;
            else{
                sum += Data[r];
                ans2 = min(ans2, sum);
            }
            r++;
        }
        printf("%d\n", max(ans1, tot - ans2));
    }
    return 0;
}

poj2100

  • 给出一个数n,使得连续正整数的平方和等于此数,输出所有可能情况
  • 考虑尺取,开始时候l和r指针都指向1,sum表示l到r闭区间内区间和,如果此时sum小于n,那么右指针右移;如果大于n,左指针右移;如果相等,跟新sum,左右指针都要向右移动,需要注意的是最后的r要减1
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 2e5+100;
int Data[MAXN];
vector<pair<ll, ll>> vs;
int main(){
    ll n;
    scanf("%lld",&n);
    ll l, r;
    ll sum = 0;
    l = r = 1;
    while(l * l <= n){
        if(sum < n){
            sum += r * r;
            r++;
        }
        else if(sum > n){
            sum -= l * l;
            l++;
        }else{
            vs.push_back(make_pair(l, r - 1));
            sum -= l * l;
            sum += r * r;
            l++;
            r++;
        }
    }
    int num = vs.size();
    printf("%d\n", num);
    for(int i=0;i<num;i++){
        printf("%lld ", vs[i].second - vs[i].first + 1);
        for(ll j=vs[i].first;j<=vs[i].second;j++){
            printf("%lld ",j);
        }
        printf("\n");
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
您好!针对您的问题,我可以为您提供一个使用动态规划和二分法解决最大子段和问题的算法。 首先,让我们定义一个函数`dp`来表示以`mid`为中点的最大子段和。我们可以使用动态规划来计算`dp`的值。 1. 首先,我们定义一个数组`sum`,其中`sum[i]`表示以索引`i`结尾的子段的和。 2. 然后,我们初始化`dp`数组,将所有元素初始化为负无穷,即`dp[i] = -∞`。 3. 接下来,我们从左到右遍历数组,计算每个`sum[i]`的值,并更新`dp[i]`的值: - 如果`i`等于0,则`dp[i]`等于`sum[i]`。 - 否则,`dp[i]`等于`max(dp[i-1] + sum[i], sum[i])`,即选择继续扩展前一个子段或者从当前位置开始一个新的子段。 4. 在计算`dp`数组的过程中,我们记录最大的`dp`值,并更新最大子段和。 接下来,我们可以使用二分法来寻找最大子段和的起始和结束位置。 1. 我们定义一个函数`binary_search`,它接受一个目标值`target`和一个排序好的数组`arr`。 2. 在`binary_search`函数中,我们使用二分法来找到第一个大于等于`target`的元素的索引。 - 初始化左指针`left`为0,右指针`right`为数组长度减一。 - 当`left`小于等于`right`时,执行以下步骤: - 计算中间指针`mid`,即`(left + right) // 2`。 - 如果`arr[mid]`小于`target`,则更新`left`为`mid + 1`。 - 否则,更新`right`为`mid - 1`。 - 返回`left`作为结果。 3. 使用`binary_search`函数找到最大子段和的起始位置和结束位置。 这就是使用动态规划和二分法解决最大子段和问题的算法。希望对您有帮助!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值