挑战程序竞赛系列(20):3.2尺取法

来源:互联网 发布:淘宝运营和天猫运营 编辑:程序博客网 时间:2024/06/03 22:04

挑战程序竞赛系列(20):3.2尺取法

详细代码可以fork下Github上leetcode项目,不定期更新。

练习题如下:

  • POJ 3061: Subsequence
  • POJ 3320: Jessica’s Reading Problem
  • POJ 2566: Bound Found
  • POJ 2739: SUm of Consecutive Prime Numbers
  • POJ 2100: Graveyard Design

POJ 3061: Subsequence

有趣的题目,刚学完二分,此题可以用二分做,时间复杂度为O(nlogn).

思路:

计算累加和,因为sum{i,j+1} > sum{i,j},一旦sum{i,j}满足条件,则不在需要求j+1后面的元素,所以我们转而去判断i可能的位置,有了sum[j],自然有sum[j] - s,把它作为key,利用二分查找最大的位置i,自然是答案。

代码如下:

public class SolutionDay26_P3061 {    InputStream is;    PrintWriter out;    String INPUT = "./data/judge/3061.txt";    void solve() {        int N = ni();        for (int i = 0; i < N; ++i){            int n = ni();            int s = ni();            int[] a = na(n);            int[] sum = new int[n+1];            for (int j = 0; j < n; ++j){                sum[j+1] = sum[j] + a[j];            }            if (sum[n] < s){                out.println(0);                continue;            }            int min = 1 << 30;            for (int j = 1; j < n + 1; ++j){                int l = binarySearch(sum, 0, j - 1, sum[j] - s);                if (l != -1){                    min = Math.min(min, j - l);                }            }            out.println(min);        }    }    public int binarySearch(int[] sum, int lf, int rt, int key){        while (lf < rt){            int mid = lf + (rt - lf + 1) / 2;            if (sum[mid] > key) rt = mid - 1;            else lf = mid;        }        if (sum[lf] <= key) return lf;        return -1;    }    void run() throws Exception {        is = oj ? System.in : new FileInputStream(new File(INPUT));        out = new PrintWriter(System.out);        long s = System.currentTimeMillis();        solve();        out.flush();        tr(System.currentTimeMillis() - s + "ms");    }    public static void main(String[] args) throws Exception {        new SolutionDay26_P3061().run();    }    private byte[] inbuf = new byte[1024];    public int lenbuf = 0, ptrbuf = 0;    private int readByte() {        if (lenbuf == -1)            throw new InputMismatchException();        if (ptrbuf >= lenbuf) {            ptrbuf = 0;            try {                lenbuf = is.read(inbuf);            } catch (IOException e) {                throw new InputMismatchException();            }            if (lenbuf <= 0)                return -1;        }        return inbuf[ptrbuf++];    }    private boolean isSpaceChar(int c) {        return !(c >= 33 && c <= 126);    }    private int skip() {        int b;        while ((b = readByte()) != -1 && isSpaceChar(b))            ;        return b;    }    private double nd() {        return Double.parseDouble(ns());    }    private char nc() {        return (char) skip();    }    private String ns() {        int b = skip();        StringBuilder sb = new StringBuilder();        while (!(isSpaceChar(b))) { // when nextLine, (isSpaceChar(b) && b != '                                    // ')            sb.appendCodePoint(b);            b = readByte();        }        return sb.toString();    }    private char[] ns(int n) {        char[] buf = new char[n];        int b = skip(), p = 0;        while (p < n && !(isSpaceChar(b))) {            buf[p++] = (char) b;            b = readByte();        }        return n == p ? buf : Arrays.copyOf(buf, p);    }    private char[][] nm(int n, int m) {        char[][] map = new char[n][];        for (int i = 0; i < n; i++)            map[i] = ns(m);        return map;    }    private int[] na(int n) {        int[] a = new int[n];        for (int i = 0; i < n; i++)            a[i] = ni();        return a;    }    private int ni() {        int num = 0, b;        boolean minus = false;        while ((b = readByte()) != -1 && !((b >= '0' && b <= '9') || b == '-'))            ;        if (b == '-') {            minus = true;            b = readByte();        }        while (true) {            if (b >= '0' && b <= '9') {                num = num * 10 + (b - '0');            } else {                return minus ? -num : num;            }            b = readByte();        }    }    private long nl() {        long num = 0;        int b;        boolean minus = false;        while ((b = readByte()) != -1 && !((b >= '0' && b <= '9') || b == '-'))            ;        if (b == '-') {            minus = true;            b = readByte();        }        while (true) {            if (b >= '0' && b <= '9') {                num = num * 10 + (b - '0');            } else {                return minus ? -num : num;            }            b = readByte();        }    }    private boolean oj = System.getProperty("ONLINE_JUDGE") != null;    private void tr(Object... o) {        if (!oj)            System.out.println(Arrays.deepToString(o));    }}

时间复杂度是否还可以优化?二分的做法,把每个sum[j] - s的查找看成独立的了,其实它们之间有着密不可分的关系,因为sum{i,j}二分求得的i是可以帮助查找sum{i’,j+1}的i’。

性质如下:

sum[j+1] - s > sum[j] - s,由此得不存在这样的i使得i<i。所以,i的更新只会往后走,而不会访问已经走过的位置。

优化代码如下:

void solve() {    int N = ni();    for (int i = 0; i < N; ++i){        int n = ni();        int s = ni();        int[] a = na(n);        int[] sum = new int[n+1];        for (int j = 0; j < n; ++j){            sum[j+1] = sum[j] + a[j];        }        if (sum[n] < s){            out.println(0);            continue;        }        int min = 1 << 30;        for (int j = 1, k = 0; j < n + 1; ++j){            int key = sum[j] - s;            if (key >= 0){                while (k < j && sum[k] <= key) k++;                min = Math.min(min, j - k + 1);            }        }        out.println(min);    }}

上述空间复杂度为O(n),它还可以优化,也就是书中介绍的尺取法,学到了。

代码如下:

void solve() {        int N = ni();        for (int i = 0; i < N; ++i){            int n = ni();            int s = ni();            int[] a = na(n);            int l = 0, j = 0, sum = 0;            int min = n + 1;            for (;;){                while (j < n && sum < s){                    sum += a[j++];                }                if (sum < s) break; //所以更新的时候,一直保证这sum > s这个条件                min = Math.min(min, j - l);                sum -= a[l++];            }            out.println(min > n ? 0 : min);        }    }

POJ 3320: Jessica’s Reading Problem

提供两种解法,但核心思想差不多。

思路:

  • 记录所有非重复知识点的个数,可以放在set中。
  • 当遇到连续的所有知识点时,更新区间大小。

滑动窗口的做法:

用Map记录知识点出现的次数,当map的大小等于知识点的个数时,说明此时的区间为合法区间,更新。但可能出现知识点重复多次的情况,从头开始,如果头指向的知识点的次数大于1次,说明可以缩减窗口,并更新。

代码如下:

    void solve() {        int n = ni();        int[] a = na(n);        Set<Integer> set = new HashSet<Integer>();        for (int i = 0; i < n; ++i) set.add(a[i]);        Map<Integer,Integer> window = new HashMap<Integer, Integer>();        int min = n;        for (int i = 0, l = 0; i < n; ++i){            if(!window.containsKey(a[i])) window.put(a[i],0);            window.put(a[i], window.get(a[i]) + 1);            if(window.size() == set.size()){                min = Math.min(min, i - l + 1);                while (l < n && window.get(a[l]) >= 2){                    min = Math.min(min, i - l);                    window.put(a[l],window.get(a[l]) - 1);                    l++;                }            }        }        out.println(min);    }

核心思想是保持合法窗口不变,在合法窗口满足的情况下更新最小区间。

尺取法:
有意思,尺取法的做法和滑动窗口有着异曲同工之妙,却又有些差别,它的第一个while循环保证,抵达下一步之前,总能找到合法窗口,当然已经是合法窗口则不需要操作,而找不到合法窗口时直接跳出循环。而更新过程中,则比较缓慢,每次自减一,可能会破坏窗口也可能不会。

代码如下:

void solve(){        int n = ni();        int[] a = na(n);        Set<Integer> set = new HashSet<Integer>();        for (int i = 0; i < n; ++i) set.add(a[i]);        Map<Integer,Integer> window = new HashMap<Integer, Integer>();        int min = n;        int l = 0, k = 0, len = set.size();        for(;;){            while (l < n && window.size() < len){                if (!window.containsKey(a[l])) window.put(a[l], 0);                window.put(a[l], window.get(a[l]) + 1);                l++;            }            if (window.size() < len) break;            min = Math.min(min, l - k);            if(window.get(a[k]) == 1){                window.remove(a[k]);                k++;            }else{                window.put(a[k], window.get(a[k]) - 1);                k++;            }        }        out.println(min);    }

POJ 2566: Bound Found

思路:
求的还是个区间和和给定的diff最小情况的最小区间,为了求连续的区间需要累加和,起码累加和能给我们连续区间和的查询操作。不用它用谁?

因为数中存在负数,所以单纯的累加和求出来的结果可以说是无序的,无序带来了一个很坏的结果,在给定某个下标j,知道了sum{j}的值,一个想法是扫描其他下标,如果存在i使得sum{i} - sum{j}的绝对值在diff范围内,且diff差距最小,则更新最小diff和区间的两个边界l和r。

我没想到排序,对绝对值的理解不够深刻,其实给了绝对值,排序后得到的结果和无序得到的结果是一样的,且有序有一个非常大的好处,在搜索某个sum{j}-diff的过程中,一定从区间[0,j-1]中搜索,这样就可以使用尺取法了么?

不急,可以先看看传统的做法,我们要找的是符合sum{j} - diff的某个sum{i},i是多少呢,可以采用二分,但二分有个问题,此题二分找到的i候选值还不是一个,因为并不是严格找sum{j}-diff的值,而是在一个很小的范围。

尺取法的好处,对于i的查找不会那么严格,类似于遍历,但遍历的顺序保证它一定是安全,一定有解存在于它的遍历顺序中。

那么尺取法为什么能够降低时间复杂度呢?很简单,尺取法每次都会确定一个上界,但每个抵达的上界在整个遍历结构中只会出现一次,为什么?

就拿这道题,寻找sum{j} - sum{i} < diff的下标i和j,但我们知道,因为sum被排序了,所以sum{j+1} - sum{i} >= sum{j} - sum{i},在j以后的值是不需要遍历的,而尺取法能够很好的做到这点。

另外一点需要注意:排序后,原本的index发生了变化,有必要记录下。

代码如下:

class Pair{        int sum;        int index;        public Pair(int sum, int index){            this.sum = sum;            this.index = index;        }        @Override        public String toString() {            return "["+sum+", "+index+"]";        }    }    Pair[] sums;    void solve(){        while (true){            int N = ni();            int Q = ni();            if (N == 0 && Q == 0) break;            int[] a = na(N);            int[] q = na(Q);            sums = new Pair[N+1];            for (int i = 0; i <= N; ++i) sums[i] = new Pair(0,i);            for (int i = 0; i < N; ++i){                sums[i+1].sum = sums[i].sum + a[i];             }            Arrays.sort(sums, new Comparator<Pair>() {                @Override                public int compare(Pair o1, Pair o2) {                    return o1.sum == o2.sum ? o2.index - o1.index : o1.sum - o2.sum;                }            });            for (int i = 0; i < Q; ++i){                int diff = q[i];                int lb = 0, ub = 0, sum = -1;                res = 0x80808080;                l = 0;                r = 0;                for(;;){                    while (ub < N && sum < diff){                        sum = getSum(lb, ++ub, diff);                    }                    if (sum < diff) break;                    sum = getSum(++lb, ub, diff);                }                out.println(res + " " + l + " " + r);            }        }    }    int res, l, r;    private int getSum(int lb, int ub, int diff){        if (lb >= ub) return -1 << 30;        int sum = sums[ub].sum - sums[lb].sum;        if (Math.abs(diff - sum) < Math.abs(diff - res)){            res = sum;            int i = sums[ub].index;            int j = sums[lb].index;            l = i < j ? i + 1 : j + 1;            r = i < j ? j : i;        }        return sum;    }

POJ 2739: SUm of Consecutive Prime Numbers

强大的技巧,真不知道是技巧指导解题,还是思路本身推出技巧,whatever,没认识到这技巧,估计我要痛苦一会。

代码如下:

int MAX_N = 10000 + 16;    int[] prime = new int[MAX_N + 1];    boolean[] isPrime = new boolean[MAX_N + 1];    int N = 0;    void seive(){        Arrays.fill(isPrime, true);        isPrime[0] = false;        isPrime[1] = false;        for (int i = 2; i <= MAX_N; ++i){            if (isPrime[i]){                prime[N++] = i;                for (int j = 2 * i; j <= MAX_N; j += i){                    isPrime[j] = false;                }            }        }    }    void solve() {        seive();        while (true){            int num = ni();            if (num == 0) break;            int lb = 0, rb = 0, cnt = 0, sum = 0;            for(;;){                while (lb < N && sum < num){                    sum += prime[rb++];                }                if (sum < num) break;                if (sum == num) cnt++;                sum -= prime[lb++];            }            out.println(cnt);        }    }

POJ 2100: Graveyard Design

题目理解起来费劲,将一个整数分解为连续数平方之和,有多少种分法?

也没难度,防止溢出,都用了long。

代码如下:

class Pair{        long l;        long r;        public Pair(long l, long r){            this.l = l;            this.r = r;        }    }    void solve() {        long num = nl();        long n = (int)Math.sqrt(num);        long lb = 1, rb = 1;        long sum = 0;        List<Pair> list = new ArrayList<Pair>();        for(;;){            while (lb <= n && sum < num){                sum += (rb * rb);                rb++;            }            if (sum < num) break;            if (sum == num){                list.add(new Pair(lb,rb - 1));            }            sum -= lb * lb;            lb++;        }        out.println(list.size());        for (Pair p : list){            long size = p.r - p.l + 1;            StringBuilder sb = new StringBuilder();            sb.append(size + " ");            for (long i = p.l; i <= p.r; ++i){                sb.append(i + " ");            }            out.println(sb.toString().trim());        }    }
原创粉丝点击