尺取法-扫描

 尺取法(又称为:双指针、two pointers),是算法竞赛中一个常用的优化技巧,用来解决序列的区间问题,操作简单、容易编程。

  如果区间是单调的,也常常用二分法来求解,所以很多问题用尺取法和二分法都行。

  另外,尺取法的的操作过程和分治算法的步骤很相似,有时候也用在分治中。

1 尺取法的概念

  什么是尺取法?为什么尺取法能优化呢?

  考虑下面的应用背景:

  (1)给定一个序列。有时候需要它是有序的,先排序。

  (2)问题和序列的区间有关,且需要操作2个变量,可以用两个下标(指针)i、j扫描区间。

  对于上面的应用,一般的做法,是用i、j分别扫描区间,有两重循环,复杂度O(n2)。以反向扫描(即i、j方向相反,后文有解释)为例,代码是:

for(int i = 0; i < n; i++)           //i从头扫到尾
for(int j = n-1; j >= 0; j--){   //j从尾扫到头
        ......
    }

  下面用尺取法来优化上面的算法。

   实际上,尺取法就是把两重循环变成了一个循环,在这个循环中一起处理i和j。复杂度也就从O(n2)变成了O(n)。仍以上面的反向扫描为例,代码是:

//用while实现:
int i = 0, j = n - 1;
while (i < j) {      //i和j在中间相遇。这样做还能防止i、j越界
        ......       //满足题意的操作
        i++;         //i从头扫到尾
        j--;         //j从尾扫到头
}
//用for实现:
for (int i = 0, j = n - 1; i < j; i++, j--) {
    ......
}

2 .反向扫描

2.1 找指定和的整数对

  这个问题是尺取法最经典,也最简单直接的应用。

∎问题描述

   输入n ( n≤100,000)个整数,放在数组a[]中。找出其中的两个数,它们之和等于整数m(假定肯定有解)。题中所有整数都是int型。

  样例输入:

  21 4 5 6 13 65 32 9 23

  28

  样例输出:

  5 23

  说明:样例输入的第一行是数组a[],第2行是m = 28。样例输出5和23,相加得28。  

∎题解

  尺取法。这是标准解法。首先对数组从小到大排序;然后,设置两个变量i和j,分别指向头和尾,i初值是0,j初值是n-1,然后让i和j逐渐向中间移动,检查a[i]+a[j],如果大于m,就让j减1,如果小于m,就让i加1,直至a[i]+a[j] = m。排序复杂度O(nlogn),检查的复杂度O(n),合起来总复杂度O(nlogn)。

  尺取法代码如下,注意可能有多个答案:

void find_sum(int a[], int n, int m){ 
     sort(a, a + n);      //先排序,复杂度O(nlogn)
     int i = 0, j = n - 1;    //i指向头,j指向尾
     while (i < j){           //复杂度O(n)
    int sum = a[i] + a[j];
    if (sum > m)   j--;
    if (sum < m)   i++;
    if (sum == m){     
    cout << a[i] << " " << a[j] << endl;  //打印一种情况
                i++;          //可能有多个答案,继续
    }
  }
}

2.2 判断回文串

  给一个字符串,判断它是不是回文串。

  例题:hdu 2029: http://acm.hdu.edu.cn/showproblem.php?pid=2029

∎问题描述

  “回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”就是回文串。写一个程序判断读入的字符串是否是“回文”。如果是,输出“yes”,否则输出“no”。

#include <bits/stdc++.h>
using namespace std;
int main(){
    int n;
    cin >> n;                         //n是测试用例个数
    while(n--){
        string s;  cin >> s;          //读一个字符串
        bool ans = true;
        int i = 0, j = s.size() - 1;  //双指针
        while(i < j){ 
            if(s[i] != s[j]){
                ans = false;
                break;
            }
            i++;   j--;               //移动双指针
        }
        if(ans)   cout << "yes" << endl;
        else      cout << "no"  << endl;
    }
    return 0;
}

3.正向扫描

3.1 寻找区间和

  这是用尺取法产生“滑动窗口”的典型例子。

∎问题描述

  给定一个长度为n的数组a[]和一个数s,在这个数组中找一个区间,使得这个区间之和等于s。输出区间的起点和终点位置。

  样例输入:

  15

  6 1 2 3 4 6 4 2 8 9 10 11 12 13 14

  6

  样例输出:

  0 0

  1 3

  5 5

  6 7

  说明:样例输入的第1行是n=15,第2行是数组a[],第3行是区间和s=6。样例输出,共有4个情况。

∎题解

  指针i和j,i<=j,都从头向尾扫描,判断区间[i,j]的和是否等于s。

  如何寻找区间和等于s的区间?如果简单地对i和j做二重循环,复杂度是O(n2)。用尺取法,复杂度O(n),操作步骤是:

  (1)初始值i=0、j=0,即开始都指向第一个元素a[0]。定义sum是区间[i, j]的和,初始值sum = a[0]。

  (2)如果sum等于s,输出一个解。继续,把sum减掉元素a[i],并把i往后移动一位。

  (3)如果sum大于s,让sum减掉元素a[i],并把i往后移动一位。

  (4)如果sum小于s,把j往后挪一位,并把sum的值加上这个新元素。

  在上面的步骤中,有2个关键技巧:

  (1)滑动窗口的实现。窗口就是区间[i,j],随着i和j从头到尾移动,窗口就“滑动”扫描了整个序列,检索了所有的数据。i和j并不是同步增加的,窗口像一只蚯蚓伸缩前进,它的长度是变化的,这个变化,正对应了对区间和的计算。

  (2)sum的使用。如何计算区间和?暴力的方法是从a[i]到a[j]累加,但是,这个累加的复杂度是O(n)的,会超时。如果利用sum,每次移动i或j的时候,只需要把sum加或减一次,就得到了区间和,复杂度是O(1)。这是“前缀和”递推思想的应用。

  下面是代码。

void findsum(int *a, int n, int s){
int i = 0, j = 0;
int sum = a[0];
while(j < n){   //下面代码中保证 i<=j
        if(sum >= s){
            if(sum == s) printf("%d %d\n", i, j);
sum -= a[i];
i++;
            if(i>j) {sum = a[i]; j++;}  //防止i超过j
}
        if(sum < s){
j++;
sum += a[j];
}
}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值