【算法练习】杂题混讲一:Acwing + 蓝桥杯练习

一、牛的学术圈Ⅰ(二分)

在这里插入图片描述

在这里插入图片描述
如果不考虑参数L的影响,那就是一个纯纯的二分答案,问题在于如何处理参数L?,仔细读题,题目说了总共至多引用L篇,每篇论文至多引用1次,至多的意思是说可以引用也可以不引用。 那我们就先不考虑L的影响,遍历所有论文,只要论文的引用次数比二分答案(指数),大或等于或者比答案小1都可以算满足,然后统计出来看总的>=h指数的论文篇数是否达到了h篇(>=),是不是很简单?这个时候再考虑L的影响,L无非是对论文引用+1的次数有限制,那我们只要在统计论文篇数时取L和之前和答案差1的论文篇数的较小值即可。

import java.util.*;

public class Main {
	static int[] cnt;
	static int N, L;
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		N = scan.nextInt();
		L = scan.nextInt();
		// N 论文篇数
		// L 能够自己引用的论文数(可以提高某个论文的引用数)至多每篇论文引用一次
		cnt = new int[N];
		for (int i = 0; i < N; i++) cnt[i] = scan.nextInt();
		int left = 0;
		// 最大h指数只能为N + 1(因为每个论文至多再+1)
		int right = N + 1;
		int mid = 0;
		int ans = 0;
		while (left <= right) {
			mid = left + (right - left) / 2;
			if (check(mid)) {
				// 当前指数可以,那就往大的找
				ans = mid;
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		System.out.println(ans);
	}
	static boolean check(int mid) {
		int tmp = 0;
		// 下面记录需要添加引用才能够的论文
		int tmpp = 0;
		for (int i = 0; i < N; i++) {
			if (cnt[i] >= mid) tmp++;
			else if (mid - cnt[i] == 1) tmpp++; 
		}
		// 最多引用L次
		tmpp = Math.min(tmpp, L);
		tmp += tmpp;
		// 当前满足mid指数的文章数>=mid指数就可以
		if (tmp >= mid) return true;
		return false;
	}
}

二、打包(二分)

在这里插入图片描述
二分答案即可,关键是二分里面的check内容不太好理解:

import java.util.*;

public class Main {
	static int n, m;
	static int[] gift;
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		n = scan.nextInt();
		m = scan.nextInt();
		gift = new int[n];
		int left = 0;
		int right = 0;
		for (int i = 0; i < n; i++) {
			gift[i] = scan.nextInt();
			left = Math.max(left, gift[i]);
			right += gift[i];
		}
		// 二分区间:[max(gift), sum(gift)]
		int mid = 0;
		int ans = 0;
		while (left <= right) {
			mid = left + (right - left) / 2;
			if (check(mid)) {
				ans = mid;
				// 最大重量最小,往小的缩小
				right = mid - 1;
			} else {
				left = mid + 1;
			}
		}
		System.out.println(ans);
	}	
	static boolean check(int mid) {
		// 当前最大重量是否满足m个包裹的需求?
		int curPackage = 0;  // 当前包裹数
		int curWeight = 0;  // 当前包裹总量
		for (int i = 0; i < n; i++) {
			// 注意要连续打包
			curWeight += gift[i];
			if (curWeight > mid) {
				// 大于最大重量的要求,重新打一个包裹
				curPackage++;
				curWeight = gift[i];
			}
		}
		// 注意,只要包裹数大于题目要求包裹数就肯定false
		if (curPackage + 1 > m) return false;
		// 但如果包裹书小于等于题目要求就说明当前mid所代表的最大重量太大了,还可以缩减
		return true;
	}
}

记需要包的数量为sum。如果sum<=m,也就是说最大重量Target拿完所有礼物绰绰有余,例如本来需要4个包才能装下的现在只需要两个,Target的值可能太大了,那么答案ans<=Target;如果sum>m,说明最大重量Target下装不完所有礼物,那只能够增大Target的值,答案ans>Target,枚举直到二分查找结束。

※三、闯关(动态规划、最大子序列和问题)

在这里插入图片描述
这道题非常有意思,它限制了必须从第一关开始,最后一关结束,然后想要到达最后一关时,取得的金钱数最大,并且限制了每次能够到达的位置关系,抛开其它所有限制条件,回到问题本身,这就是一道:求数组最大子序列和的问题(子序列指保证数组顺序的相对关系,可以删除数组中某一个、几个元素,也可以不删除,但不能改变数组的顺序)

dp[i]表示以第 i 关结束的最多能够获取的金币数,我们可以初始化所有的dp[i] = 第一关能够获取到的金币数量,因为第一关必须打,然后对于后面每一关,我们遍历可以从前面跳转到该位置的关卡(可以从第一关、第二关…当然要满足题目对m的限制),直到遍历到最后一关,我们需要的答案就是dp[n]。

import java.util.*;

public class Main {
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		int n = scan.nextInt();
		int m = scan.nextInt();
		int p = scan.nextInt();
		long[] v = new long[n + 1];
		for (int i = 1; i <= n; i++) {
			v[i] = scan.nextLong();
			// 提前计算好每一关能够赚的金币数
			v[i] -= p;
		}
		// 必须从第1关打起
		long[] dp =new long[n + 1];
		// 每个位置必须从第1关开始
		Arrays.fill(dp, v[1]);
		// dp[i]: 以第i关结尾的最多金钱数
		for (int i = 2; i <= n; i++) {
			// 必须从第一关开始打起,所以第一关就不用考虑
			int begin = Math.max(1, i - m);
			// 遍历前面能够到达关卡i的关卡j
			long max = Long.MIN_VALUE;
			for (int j = begin; j <= i - 1; j++) {
				max = Math.max(max, dp[j] + v[i]);
			}
			dp[i] = max;
		}
		System.out.println(dp[n]);
	}	
}

这道题真的很巧妙,用的模板就是很老套、很熟悉的模板,但是如果不细细品味,很难发掘这道题的思路。

※四、怪物森林(二分答案+BFS)

在这里插入图片描述
题目要求是找到所有路径中,每条路径中的最小值的最大值,就是先找每条路径的最小值,然后保证这个最小值在所有路径中最大。尝试过很多方法,包括优先队列啥的,发现没用,后面看了大佬的方法,太牛了!二分答案,然后再用BFS去判断,看当前的攻击力是否能够达到终点即可。

这道题用C++可以跑过,但是Java由于本身System.out.println读入读出很慢,所以后面几个点超时了,思路是正确的。

import java.util.*;

class node {
	int x, y;
	node(){}
	node (int x, int y) {
		this.x = x;
		this.y = y;
	}
}
public class Main {
	static int[] x = new int[] {0,0,1,-1};
	static int[] y = new int[] {1,-1,0,0};
	static int[][] monster;
	static boolean[][] vis;
	static Queue<node> queue;
	static int n, m;
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		n = scan.nextInt();
		m = scan.nextInt();
		monster = new int[n][m];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				monster[i][j] = scan.nextInt();
			}
		}
		int left = -1000000000;
		int right = monster[0][0];
		int mid = 0;
		int ans = 0;
		// 二分答案
		while (left <= right) {
			mid = left + (right - left) / 2;
			if (check(mid)) {
				ans = mid;
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		System.out.println(ans);
	}
	static boolean check(int mid) {
		// BFS广搜
		queue = new LinkedList<>();
		vis = new boolean[n][m];
		queue.offer(new node(0, 0));
		vis[0][0] = true;
		while (!queue.isEmpty()) {
			node tmp = queue.poll();
			for (int i = 0; i < 4; i++) {
				int tx = tmp.x + x[i];
				int ty = tmp.y + y[i];
				if (tx < 0 || ty < 0 || tx >= n || ty >= m || vis[tx][ty]) continue;
				// 二分的是答案,也就是路径上最小的攻击力,不能有攻击力比mid还小,否则肯定不能走这里
				if (monster[tx][ty] < mid) continue;
				vis[tx][ty] = true;
				queue.offer(new node(tx, ty));
			}
		}
		// 判断当前攻击力是否能够访问到终点
		return vis[n - 1][m - 1];
	}
}

※五、智能体系列赛(DFS搜搜)

在这里插入图片描述
刚开始想的是最小生成树,发现不对劲啊,这东西要求必须从起点开始,最小生成树只是说连通所有节点就行,很明显不对,发现问题后赶紧转变思路,因为每个点都能到任意点,所以每次dfs搜索需要遍历所有点,当然要用vis数组标记。

import java.beans.Visibility;
import java.util.*;

public class Main {
	static int[] x = new int[] {0,0,1,-1};
	static int[] y = new int[] {1,-1,0,0};
	static int ans = Integer.MAX_VALUE;
	static int n;
	static boolean[] vis;
	static int[][] mine;
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		int xi = scan.nextInt();
		int yi = scan.nextInt();
		n = scan.nextInt();  // 矿点数
		// 读取每个矿点坐标,把边存一下
		mine = new int[n][2];
		vis = new boolean[n];
		for (int i = 0; i < n; i++) {
			mine[i][0] = scan.nextInt();
			mine[i][1] = scan.nextInt();
		}
		// 注意呀vis数组中压根没存开始位置,所以开始位置只会被遍历1次
		dfs(xi, yi, 0, 0);
		System.out.println(ans);
	}
	static void dfs(int x, int y, int cnt, int step) {
		if (step > ans) return;
		if (cnt == n) {
			// 所有位置遍历完了
			ans = Math.min(ans, step);
			return;
		}
		// 遍历可能去到的点的位置
		for (int i = 0; i < n; i++) {
			if (vis[i]) continue;
			step += Math.abs(x - mine[i][0]) + Math.abs(y - mine[i][1]);
			cnt++;  // 去过的地方++
			vis[i] = true;
			dfs(mine[i][0], mine[i][1], cnt, step);
			// 回溯
			vis[i] = false;
			cnt--;
			step -= Math.abs(x - mine[i][0]) + Math.abs(y - mine[i][1]);
		}
	}
}

※六、秘密行动(动态规划)

在这里插入图片描述
在每一层它可以向上跳1、2层(直接是跳1、2层楼),也可以靠花费1个单位时间走1高度(每个楼层有各自的高度),现在说:跳了之后,必须靠走,才能继续跳,问到达顶层需要至少多少时间?

考虑当前层 i,它可以由上一层跳、或者走上来,所以这就有两种状态,一个是楼层数,一个是路径方法,所以是个二维DP。

import java.util.*;

public class Main {

	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		int n = scan.nextInt();
		int[] hei = new int[n + 1];
		for (int i = 1; i <= n; i++) hei[i] = scan.nextInt();
		int[][] dp = new int[n + 1][2];
		dp[1][0] = 0;  // 第一层楼靠跳
		dp[1][1] = hei[1];  // 第一层楼靠走
		for (int i = 2; i <= n; i++) {
			// 到达这一层靠走,那可以跳完走,也可以走了继续走,反正只可能从上一层转换过来
			dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][1]) + hei[i];  // 走的话必须要把这一层楼走完
			// 到达这一层靠跳,那前面必须是走,跳的话可以跳1、2层
			dp[i][0] = Math.min(dp[i - 1][1], dp[i - 2][1]);
		}
		System.out.println(Math.min(dp[n][1], dp[n][0]));
	}
}

七、搬运冰块(贪心)

在这里插入图片描述
注意本题,冰块在搬运过程中是不融化的!看样例输入,我们按照冰块ti / di的比值,从小到大排序即可。

import java.util.*;

class ice {
	int t, d;
	ice(){}
	ice(int t, int d) {
		this.t = t;
		this.d = d;
	}
}
public class Main {

	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		int n = scan.nextInt();
		ice[] ices = new ice[n];
		for (int i = 0; i < n; i++) {
			int tt = scan.nextInt();
			int dd = scan.nextInt();
			ices[i] = new ice(tt, dd);
		}
		Arrays.sort(ices, new Comparator<ice>() {
			@Override
			public int compare(ice o1, ice o2) {
				int t1 = o1.t;
				int d1 = o1.d;
				int t2 = o2.t;
				int d2 = o2.d;
				// t/d 两个分数比较大小
				t1 *= d2;
				t2 *= d1;
				int tmp = d1;
				d1 *= d2;
				d2 *= tmp;
				return t1 - t2;
			}
		});
		long cnt = 0;
		for (int i = 0; i < n; i++) {
			int t = ices[i].t;
			for (int j = i + 1; j < n; j++) {
				cnt += (long)(t * ices[j].d);
			}
		}
		System.out.println(cnt);
	}
}

八、Cat And Mouse(模拟搜索)

在这里插入图片描述
让猫和老鼠一起动,遇到障碍物就按题目意思进行转向,这里必须要猫鼠一起动,单靠猫、鼠一个人动,是不能找到递归出口的!导致一直递归下去,StackOverflow。

import java.util.*;

public class Main {
	static char[][] map;
	public static void main(String[] args) {

/**


*...*.....
......*...
...*...*..
..........
...*.C....
*.....*...
...*......
..M......*
...*.*....
.*.*......


 */
		Scanner scan = new Scanner(System.in);
		map = new char[10][10];
		// 记录到达每个点的时间
		// 读入地图,并记录猫、老鼠的起点
		int cati = 0, catj = 0;
		int mousei = 0, mousej = 0;
		for (int i = 0; i < 10; i++) {
			map[i] = scan.nextLine().toCharArray();
			for (int j = 0; j < 10; j++) {
				if (map[i][j] == 'C') {
					cati = i;
					catj = j;
				}
				if (map[i][j] == 'M') {
					mousei = i;
					mousej = j;
				}
			}
		}
		// 分两次遍历猫、老鼠行进路线即可
//		平时沿直线走,下一步如果会走到障碍物上去或者出界,就用1秒的时间做一个右转90°。一开始它们都面向北方。
		dfs(cati, catj, mousei, mousej, 0, 0, 0);
	}
	// 猫的坐标、老鼠的坐标
	static void dfs(int i, int j, int k, int t, int dirC, int dirM, int time) {
		if (i == k && j == t) {
			// 相遇了
			System.out.println(time);
			return;
		}
		int ti = i, tj = j, tk = k, tt = t;
		if (dirC == 0) {
			if (i - 1 < 0 || map[i - 1][j] == '*') {
				// 转向不移动
				dirC = 3;
			} else {
				// 可以走
				ti -= 1;
			}
		} else if (dirC == 1) {
			// 左走
			if (j - 1 < 0 || map[i][j - 1] == '*') {
				// 转向不移动
				dirC = 0;
			} else {
				// 可以走
				tj -= 1;
			}
		} else if (dirC == 2) {
			// 下走
			if (i + 1 >= 10 || map[i + 1][j] == '*') {
				// 转向不移动
				dirC = 1;
			} else {
				// 可以走
				ti += 1;
			}
		} else {
			// 右走
			if (j + 1 >= 10 || map[i][j + 1] == '*') {
				// 转向不移动
				dirC = 2;
			} else {
				// 可以走
				tj += 1;
			}
		}
		// 老鼠的位置  i j k t
		if (dirM == 0) {
			if (k - 1 < 0 || map[k - 1][t] == '*') {
				// 转向不移动
				dirM = 3;
			} else {
				// 可以走
				tk -= 1;
			}
		} else if (dirM == 1) {
			// 左走
			if (t - 1 < 0 || map[k][t - 1] == '*') {
				// 转向不移动
				dirM = 0;
			} else {
				// 可以走
				tt -= 1;
			}
		} else if (dirM == 2) {
			// 下走
			if (k + 1 >= 10 || map[k + 1][t] == '*') {
				// 转向不移动
				dirM = 1;
			} else {
				// 可以走
				tk += 1;
			}
		} else {
			// 右走
			if (t + 1 >= 10 || map[k][t + 1] == '*') {
				// 转向不移动
				dirM = 2;
			} else {
				// 可以走
				tt += 1;
			}
		}
		dfs(ti, tj, tk, tt, dirC, dirM, time + 1);
	}
}

九、答疑(贪心)

在这里插入图片描述
在这里插入图片描述
按照题目意思,因为后面的同学会一直等待前面在问问题的同学,也就是说前面的同学问问题的时间越久,后面同学等的越久,累计总和越大,所以,我们可以按照三个同学的三个时间累加和,从小到大排序,这样就可以保证后面同学的等待时间较短。

import java.util.*;

class stu {
	int s, a, e;
	stu(){}
	stu(int s, int a, int e) {
		this.s = s;
		this.a = a;
		this.e = e;
	}
}
public class Main {
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		int n = scan.nextInt();
		stu[] S = new stu[n];
		for (int i = 0; i < n; i++) {
			S[i] = new stu();
			S[i].s = scan.nextInt();
			S[i].a = scan.nextInt();
			S[i].e = scan.nextInt();
		}
		// 按照时间总和从小到大排序,因为后面发消息的时刻一定会累加前面发消息的时刻,包括其它的等待时间
		// 所以让总和从小到大排序,保证每个人的发消息时间都小
		Arrays.sort(S, new Comparator<stu>() {
			@Override
			public int compare(stu o1, stu o2) {
				return (o1.s + o1.a + o1.e) - (o2.s + o2.a + o2.e);
			}
		});
		// 计算发消息的时刻总和
		long ans = 0;
		long curTime = 0;
		for (int i = 0; i < n; i++) {
			curTime = curTime + S[i].a + S[i].s;
			ans += curTime;
			curTime += S[i].e;
		}
		System.out.println(ans);
	}
}

十、矩阵翻转

在这里插入图片描述
这道题试了DFS,但是超时了,还没看懂网上大佬咋做的…

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@u@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值