参考
【算法2-2】常见优化技巧 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
- P1102 A-B 数对
- P1638 逛画展
- P1115 最大子段和
- P7072 [CSP-J2020] 直播获奖
- P2671 [NOIP2015 普及组] 求和
- P4147 玉蟾宫
- P2866 [USACO06NOV] Bad Hair Day S
- P1950 长方形
- P2032 扫描
- P2216 [HAOI2007] 理想的正方形
- UVA11572 唯一的雪花 Unique Snowflakes
- P4653 [CEOI2017] Sure Bet
- P3143 [USACO16OPEN] Diamond Collector S
- P7910 [CSP-J 2021] 插入排序
- P1578 奶牛浴场
- P3467 [POI2008] PLA-Postering
- P1886 滑动窗口 /【模板】单调队列
- P2880 [USACO07JAN] Balanced Lineup G
- P1714 切蛋糕
- P1725 琪露诺
双指针
一般来说双指针可以优化原本需要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博客
就是栈里维护一个单调序列:可解题型如下,往一边找第一个比自身大/小的值
以这道题为例,都是往右边找第一个>=自身(条件)的数,所以从后往前遍历。
- 如果栈空,直接保存结果-1;
- 如果栈顶元素满足条件(>=自身),直接保存结果为栈顶元素(此题要的是下标,比较的时候用的是下标对应值);
- 如果栈顶元素不满足条件(<自身),则出栈直到情况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博客
单调队列通常解决动态小区间中寻找极值问题。
- 动态区间找最小值,使用单调递增序列,队首就是最小值;
- 动态区间找最大值,使用单调递减序列,队首就是最大值;
中间操作都是类似的,例如找最小值,要维护单调递增序列
如果当前遍历到的值违反了队列的单调要求,那么就要把元素从队尾出队,每次都要把新元素从队尾入队。
如果当前队列中的元素的下标已经不在滑动窗口内了,则这个值从队首出队。
为什么最小值是维护单调递增序列,可以见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] 理想的正方形
都是提高题