备战蓝桥杯,用JAVA刷洛谷算法题单:【算法2-2】常见优化技巧

文章讨论了如何利用双指针优化算法,降低时间复杂度,包括在数对问题、画展参观者选择、直播获奖排名和区间最值计算中的应用,以及空间换时间策略如桶排序。文中提及的题目大多涉及竞赛编程,如NOIP、USACO和HAOI等。
摘要由CSDN通过智能技术生成

参考

【算法2-2】常见优化技巧 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

image-20240314135443776

双指针

一般来说双指针可以优化原本需要O(n2)时间的遍历+求连续区间满足某一条件的这种问题,而且数据规模比较大

P1102 A-B 数对

1-6二分做过。

package _1_6;

import java.io.*;
import java.util.Arrays;

public class P1102 {

    private static StreamTokenizer in;
    private static PrintWriter out;
    private static long[] nums;
    private static long c;
    private static int n;

    public static void main(String[] args) throws IOException {
        in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        out = new PrintWriter(System.out);

        in.nextToken();
        n = (int) in.nval;
        in.nextToken();
        c = (long) in.nval;
        nums = new long[n];
        for (int i = 0; i < n; i++) {
            in.nextToken();
            nums[i] = (long) in.nval;
        }

        // 先排序
        Arrays.sort(nums);
        // algo 双指针
        long count = 0;
        int l = 0, r = 0;
        for (int i = 0; i < n; i++) {
            // 找到第一个nums[l]-nums[i]>=c,nums[r]-nums[i]>c,则l~r-1区间就是-nums[i]==c
            while (l < n && nums[l] - nums[i] < c) l++;
            while (r < n && nums[r] - nums[i] <= c) r++;
            // 如果l==n则表示找不到对应值
            if (l < n && nums[l] - nums[i] == c && r >= 1 && nums[r - 1] - nums[i] == c)
                count += r - l;
        }
        out.println(count);

        out.flush();
        out.close();
    }
}

P1638 逛画展

看着n的数量级,只能O(n)时间才AC,可以用双指针

package _2_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;

public class P1638 {
	private static StreamTokenizer in;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

		int n = nextInt();
		int m = nextInt();
		int[] draws = new int[n + 1];
		// visited存当前范围包含了画师i的次数
		int[] visited = new int[m + 1];
		// cnt表示包含了画师的种数
		int cnt = 0;
		for (int i = 1; i <= n; i++) {
			draws[i] = nextInt();
		}

		// algo 双指针
		int l, r;
		l = 1;
		r = l - 1;
		// r右移一直到包含所有画师
		while (cnt != m && r <= n - 1) {
			r++;
			// 如果当前这幅画的画师没有被包含,那就加入
			if (visited[draws[r]] == 0) {
				cnt++;
			}
			visited[draws[r]] += 1;
		}
		// 去除左端重复的画师
		while (r - l + 1 >= m && visited[draws[l]] > 1) {
			visited[draws[l]]--;
			l++;
		}
		if (r - l + 1 == m) {
			System.out.printf("%d %d", l, r);
			return;
		}
		// 此时lr区间已经满足了条件包含所有画师的目前最短,但是也有可能后续还有更短的区间
		// 继续r+1,每次去除左端重复,如果区间更小了那就更新(如果相同不更新,因为多组解得l更小的)
		int minl = l, minr = r;
		while (r <= n - 1) {
			r++;
			visited[draws[r]] += 1;
			// 去除左端重复的画师
			while (r - l + 1 >= m && visited[draws[l]] > 1) {
				visited[draws[l]]--;
				l++;
			}
			// 更新更短的区间
			if (r - l < minr - minl) {
				minr = r;
				minl = l;
			}
		}
		System.out.printf("%d %d", minl, minr);

	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

空间换时间

(题外话:之前不是有一道异或题吗,4Mb的空间Java根本AC不了)

P7072 [CSP-J2020] 直播获奖

首先想到的就是每次读入选手的成绩后排序,然后求出前w%个人的最低成绩。

Java的Arrays.sort用了一个TimSort,时间复杂度是O(ologn)。不过注释说如有部分排序了,那这个算法的实际时间远低于nlgn。

不过这样的话,这个朴素的写法的时间复杂度是O(n2logn),可以过104的数量级,但是后面三个105就TLE。

这里有个注意的点,Arrays.sort(T[],start,end,Comparator)这里的数组要用包装类的

package _2_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;
import java.util.Comparator;

public class P7072 {
	private static StreamTokenizer in;
	private static PrintWriter out;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		out = new PrintWriter(System.out);

		int n = nextInt();
		double w = nextInt() * 0.01;
		Integer[] scores = new Integer[n + 1];
		for (int i = 1; i <= n; i++) {
			// 当前i个人,前w%就是i*0.01*w个人,然后再获得最后一个人的成绩
			scores[i] = nextInt();
			// 降序排序,这里要用Integer包装类,所以也可以选择从后面往前面选
			Arrays.sort(scores, 1, i + 1, Comparator.reverseOrder());
			int t = (int) (i * w);
			if (t < 1) {
				t = 1;
			}
			out.printf("%d ", scores[t]);
		}

		out.flush();
		out.close();
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

关键点在排序时间上,可以使用时间复杂度为O(n)的桶排序:点题用空间换时间,1-2曾做到过一道桶排序。因为桶排序不是比较排序,所以不受O(ologn)的限制(比较排序这个时间复杂度是最优的了)。还有一点,题中的值范围<=600,所以空间也不用开很大,也可以想到桶排序。

package _2_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;
import java.util.Comparator;

public class P7072 {
	private static StreamTokenizer in;
	private static PrintWriter out;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		out = new PrintWriter(System.out);

		int n = nextInt();
		double w = nextInt() * 0.01;
		int[] scores = new int[605];
		for (int i = 1; i <= n; i++) {
			scores[nextInt()]++;
			// 当前i个人,前w%就是i*0.01*w个人,然后再获得最后一个人的成绩
			int t = (int) (i * w);
			if (t < 1) {
				t = 1;
			}
			// 找第t个人的分数(注意降序)
			int j;
			for (j = 604; j >= 0; j--) {
				if (scores[j] > 0) {
					// 这里会对scores操作,所以一次性-去,这样就不会改变scores了,反正判断也是t<=0
					t -= scores[j];
				}
				// 因为可能重分,所以t<=0的时候也算找到分数线
				if (t <= 0) {
					out.printf("%d ", j);
					break;
				}
			}

		}

		out.flush();
		out.close();
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

P2671 [NOIP2015 普及组] 求和

按照颜色分组想到了,把等式移相一下没想到。

单调栈

P2866 [USACO06NOV] Bad Hair Day S

什么是单调栈?单调栈(C/C++)-CSDN博客

就是栈里维护一个单调序列:可解题型如下,往一边找第一个比自身大/小的值

image-20240315121219633

以这道题为例,都是往右边第一个>=自身(条件)的数,所以从后往前遍历。

  1. 如果栈空,直接保存结果-1;
  2. 如果栈顶元素满足条件(>=自身),直接保存结果为栈顶元素(此题要的是下标,比较的时候用的是下标对应值);
  3. 如果栈顶元素不满足条件(<自身),则出栈直到情况1或2。
package _2_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.LinkedList;

public class P2866 {
	private static StreamTokenizer in;
	private static int n;
	private static int[] hs;
	private static LinkedList<Integer> stack;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		n = nextInt();
		hs = new int[n + 1];

		stack = new LinkedList<Integer>();

		for (int i = 1; i <= n; i++) {
			hs[i] = nextInt();
		}
		long cnt = 0;
		// 注意从后往前遍历
		for (int i = n; i >= 0; i--) {
			int t = findHeigher(i);
			if (t == -1) {
				cnt += n - i;
			} else {
				cnt += t - i - 1;
			}
		}
		System.out.println(cnt);
	}

	// algo 单调栈
	public static int findHeigher(int i) {
		// 弹出栈顶,直到满足情况1或2,返回结果(注意栈里存的是下标),压入当前值
		while (stack.size() > 0 && hs[stack.getFirst()] < hs[i]) {
			stack.removeFirst();
		}
		// 栈空,返回结果-1,压入当前值
		if (stack.size() == 0) {
			stack.addFirst(i);
			return -1;
		} else {
			// 这里要先获取栈顶元素,再压栈
			int t = stack.getFirst();
			stack.addFirst(i);
			return t;
		}
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

单调队列

P1886 滑动窗口 /【模板】单调队列

什么是单调队列?单调队列 - Lan_Sky - 博客园 (cnblogs.com)、[数据结构]–单调队列-CSDN博客

单调队列通常解决动态小区间中寻找极值问题

  • 动态区间找最小值,使用单调递增序列,队首就是最小值;
  • 动态区间找最大值,使用单调递减序列,队首就是最大值;

中间操作都是类似的,例如找最小值,要维护单调递增序列

image-20240315124842254

如果当前遍历到的值违反了队列的单调要求,那么就要把元素从队尾出队,每次都要把新元素从队尾入队。

如果当前队列中的元素的下标已经不在滑动窗口内了,则这个值从队首出队

为什么最小值是维护单调递增序列,可以见2。

  • 因为窗口在滑动,所以即使目前这个值不是最小的,但是窗口滑动之后他有可能成为最小的(因为他被放在了极小的后面,窗口又向右边滑动),所以要存起来。
  • 但是如果当前这个值比队列一些元素都小,放入队列后即使找最小值也已经不可能再找队首那部分的值了(都有更小的了),所以前面的要出队。
package _2_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.LinkedList;

public class P1886 {
	private static StreamTokenizer in;
	private static PrintWriter out;
	private static LinkedList<Integer> queue;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		out = new PrintWriter(System.out);
		int n = nextInt();
		int k = nextInt();
		int[] num = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			num[i] = nextInt();
		}
		// algo 单调队列
		queue = new LinkedList<Integer>();
		// 求最小值,维护递增队列
		for (int i = 1; i <= n; i++) {
			// 把队首部分,不在滑动窗口内的值去掉,这里i属于窗口右边界:窗口部分i-k+1~i
			while (queue.size() > 0 && queue.getFirst() < (i - k + 1)) {
				queue.removeFirst();
			}
			// 从队尾开始维护队列单调递增
			while (queue.size() > 0 && num[queue.getLast()] > num[i]) {
				queue.removeLast();
			}
			// 入队
			queue.add(i);
			// 输出当前最值,注意右边界i从k开始才算一个完整窗口
			if (i >= k) {
				out.printf("%d ", num[queue.getFirst()]);
			}
		}
		out.println();
		queue.clear();
		// 求最大值,维护递减队列
		for (int i = 1; i <= n; i++) {
			// 把队首部分,不在滑动窗口内的值去掉,这里i属于窗口右边界:窗口部分i-k+1~i
			while (queue.size() > 0 && queue.getFirst() < (i - k + 1)) {
				queue.removeFirst();
			}
			// 从队尾开始维护队列单调递减
			while (queue.size() > 0 && num[queue.getLast()] < num[i]) {
				queue.removeLast();
			}
			// 入队
			queue.add(i);
			// 输出当前最值,注意右边界i从k开始才算一个完整窗口
			if (i >= k) {
				out.printf("%d ", num[queue.getFirst()]);
			}
		}

		out.flush();
		out.close();
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

60%AC,有几个TLE,那换静态数组?

package _2_2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.LinkedList;

public class P1886 {
	private static StreamTokenizer in;
	private static PrintWriter out;
	private static LinkedList<Integer> queue;
	private static int n;
	private static int k;
	private static int[] nums;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		out = new PrintWriter(System.out);
		n = nextInt();
		k = nextInt();
		nums = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			nums[i] = nextInt();
		}
		fun2();

		out.flush();
		out.close();
	}

	// algo 单调队列:静态数组版
	public static void fun2() {
		int[] queue = new int[n + 1];
		// 双指针指向队首尾,此时size=tail-head+1,所以表示空即tail+1=head
		int head = 1, tail = 0;
		// 求最小值,维护递增队列
		for (int i = 1; i <= n; i++) {
			// 把队首部分,不在滑动窗口内的值去掉,这里i属于窗口右边界:窗口部分i-k+1~i
			while (tail - head + 1 > 0 && queue[head] < (i - k + 1)) {
				head++;
			}
			// 从队尾开始维护队列单调递增
			while (tail - head + 1 > 0 && nums[queue[tail]] > nums[i]) {
				tail--;
			}
			// 入队
			queue[++tail] = i;
			// 输出当前最值,注意右边界i从k开始才算一个完整窗口
			if (i >= k) {
				out.printf("%d ", nums[queue[head]]);
			}
		}
		out.println();
		queue = new int[n + 1];
		head = 1;
		tail = 0;
		// 求最大值,维护递减队列
		for (int i = 1; i <= n; i++) {
			// 把队首部分,不在滑动窗口内的值去掉,这里i属于窗口右边界:窗口部分i-k+1~i
			while (tail - head + 1 > 0 && queue[head] < (i - k + 1)) {
				head++;
			}
			// 从队尾开始维护队列单调递增
			while (tail - head + 1 > 0 && nums[queue[tail]] < nums[i]) {
				tail--;
			}
			// 入队
			queue[++tail] = i;
			// 输出当前最值,注意右边界i从k开始才算一个完整窗口
			if (i >= k) {
				out.printf("%d ", nums[queue[head]]);
			}
		}
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

就多了一个AC

区间最值

P4147 玉蟾宫

P1950 长方形

P2216 [HAOI2007] 理想的正方形

都是提高题

浅谈用极大化思想解决最大子矩形问题-CSDN博客

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值