【算法练习】杂题选讲(跳石头、蓝肽子序列、二倍数对数组、阶乘约数、发现环、公约数)

一、跳石头(二分)

在这里插入图片描述
在这里插入图片描述
又是没看出来的二分,二分题目藏的好深啊,主要是难在二分的check函数上,不好写。

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

public class Main {
    static int[] dist;
    static int n, m;
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        int l = scanner.nextInt();
        n = scanner.nextInt();
        m = scanner.nextInt();
        // L起点到终点的距离
        // n起点和终点间岩石数
        // m最多可以移去的岩石数
        dist = new int[n];
        // 记录每块石头到起点的距离
        for (int i = 0; i < n; i++) dist[i] = scanner.nextInt();
        int left = 1;
        int right = l;
        int mid = 0;
        int ans = 0;
        // 注意最小为left,最大为right,因为left = mid + 1,right = mid - 1
        // !!所以必须left <= right
        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 d) {
        // 搬走的石头数
        int cnt = 0;
        // 起点位置=0
        int pos = 0;
        for (int i = 0; i < n; i++) {
            if (dist[i] - pos < d) {
                // 距离小于d说明必须要搬走
                cnt++;
            } else {
                // 比d大就不用搬走,可以跳到这里,同时pos也要更新
                pos = dist[i];
            }
        }
        // 看搬走的石头数是否超过至多的石头数
        return cnt <= m;
    }
}

二、蓝肽子序列(DP)

在这里插入图片描述
在这里插入图片描述
题目有点类似于最长公共子序列,但是不是以一个个字符为单位,而是以一个个蓝肽为单位,一个蓝肽也就是肽链的一个子串,以大写字母打头,后续跟着若干个小写字母,也就是说,我们要先把肽链中的蓝肽分割出来。

好了,分割出来了,我们把这一个个蓝肽当作一个单位,就当作是一个字符吧,现在要求最长公共子序列的最大长度,那不就是经典的DP模型?

回忆一下最长公共子序列的转移方程:

for (int i = 1; i <= n; i++) {
	for (int j = 1; j <= m; j++) {
		if (str[i] == str[j]) {
			// 相等的话那就不用删字符,从前面的状态转移过来即可
			dp[i][j] = dp[i - 1][j - 1] + 1;
		} else {
			// 不相等可以选择删s1的字符,也可以选择删s2的字符
			dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
		}
	}
}

三、谈判(优先队列)

在这里插入图片描述
先把人数少的村庄进行谈判,把人数多的村庄留到后面谈,也就是优先队列。(本来是和合并石子一样的题目,发现并不是哦,因为两个村庄的合并是任意的,而没有顺序要求)

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

public class Main {
	public static void main(String[] args) throws IOException {
		Scanner in = new Scanner(System.in);
		int n = in.nextInt();
		PriorityQueue<Integer> pq = new PriorityQueue<>();
		for (int i = 0; i < n; i++) {
			int cur = in.nextInt();
			pq.offer(cur);
		}
		int sum = 0;
		while (pq.size() != 1) {
			int x = pq.poll();
			int y = pq.poll();
			sum += x + y;
			pq.offer(x + y);
		}
		System.out.println(sum);
	}
}

四、幸运数(模拟)

在这里插入图片描述
在这里插入图片描述
模拟整个删除的过程即可,外层循环遍历应该删除的下标,内层循环遍历现在有的数,在内层循环遍历过程中记录每个数的新的下标。

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

public class Main {
	public static void main(String[] args) throws IOException {
		Scanner in = new Scanner(System.in);
		int m = in.nextInt();
		int n = in.nextInt();
		boolean[] vis = new boolean[1000001];
		for (int i = 2; i <= n; i++) {
			if (vis[i]) continue;  // 已经被删除了
			int cnt = 0;  // 记录下标
			for (int j = 1; j <= n; j++) {
				// 遍历每个数,确定它们当前的下标值
				if (vis[j] == false) {
					cnt++;  
					if (cnt % i == 0) {
						vis[j] = true;  // 当前数又被删掉了
					}
				}
			}
		}
		int ans = 0;
		for (int i = m + 1; i < n; i++) {
			if (vis[i] == false) ans++;
		}
		System.out.println(ans);
	}
}

五、二倍数对数组(中等)

在这里插入图片描述
先排序,例如:4,-2,2,-4变成-4,-2,2,4,如果是负数应该是第二个数的两倍等于第一个数,如果是正数,应该是第一个数的两倍等于第二个数,所以用队列进行模拟即可。

class Solution {
    public boolean canReorderDoubled(int[] arr) {
    	Arrays.sort(arr);
        Queue<Integer> queue = new LinkedList<>();
        for (int cur : arr) {
        	if (queue.isEmpty()) {
        		queue.offer(cur);
        	} else if (queue.peek() * 2 == cur || cur * 2 == queue.peek()) {
        		queue.poll();
        	} else {
        		queue.offer(cur);
        	}
        }
        return queue.isEmpty();
    }
}

※六、阶乘约数(约数问题)

在这里插入图片描述
(不同)约数个数问题是常考的问题,我们知道一个数可以唯一的表示为,某些质因数的乘积:
在这里插入图片描述
1的约束个数=1,2的约束个数=2(1,2),3的约束个数=2(1,3),4 = 1 * 2 * 2 = 2^2,所以4的约数个数=3(1,2,4),5 = 1 * 5,其约数个数=2(1,5),注意这里求的约数个数都是不同的约数个数,并且每个约数都是正约数(>=1)。

注意,1不是质数,所以质因子就不可能有1

有了上面的数学定理后,我们可以把1 - 100每个数做质因数分解,最后分解完100个数后,再去看每个质因数(从2开始)的指数,然后用公式(1 + a1) * (1 + a2) * (1 + a3) … * (1 + ak)算出来即可。

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

public class Main {
	public static void main(String[] args) {
		int[] cnt = new int[110];
		for (int i = 2; i <= 100; i++) {
			int tmp = i;
			// 做质因数分解
			for (int j = 2; j <= Math.sqrt(tmp); j++) {
				while (tmp % j == 0) {
					cnt[j]++;
					tmp /= j;
				}
			}
			if (tmp != 1) {
				// 说明是质数
				cnt[tmp]++;
			}
		}
		long ans = 1;
		// 按照质因数分解的质数求约数个数
		for (int i = 2; i <= 100; i++) {
			if (cnt[i] > 0) {
				ans *= (1 + cnt[i]);
			}
		}
		System.out.println(ans);
	}
}

答案:39001250856960000

七、含2天数(日期问题)

在这里插入图片描述
日期问题最关键的是对闰年的判断,能够被4整除但不能被100整除,或者能够被400整除,就是闰年,否则不是。闰年的特点就在于,该年的天数要多一天,变成366天,该年的2月份要多1天是29天,就只有这些区别。

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

public class Main {
	// 闰年判断
	static boolean isLeap(int year) {
		if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
			return true;
		}
		return false;
	}
	// 判断年份中是否有2
	static boolean check(int year) {
		while (year != 0) {
			if (year % 10 == 2) return true;
			year /= 10;
		}
		return false;
	}
	public static void main(String[] args) {
		int ans = 0;
		int[] month = new int[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
		for (int i = 1900; i <= 9999; i++) {
			if (check(i)) {
				// 月份含有2那就一整年都算
				if (isLeap(i)) ans += 366;
				else ans += 365;
				// 闰年一年366,普通年一年365
			} else {
				// 如果当前年没有包含2,那就看月份有没有包含2
				for (int j = 1; j <= 12; j++) {
					if (check(j)) {
						// 如果包含2
						ans += month[j];
						if (j == 2 && isLeap(i)) ans++;  // 如果是闰年2月还要再加1天
					} else {
						// 月份也没有2,那就看日期
						int day = month[j];
						if (j == 2 && isLeap(i)) day++;
						for (int k = 1; k <= day; k++) {
							if (check(k)) {
								// 这一天是有2的
								ans++;
							}
						}
					}
				}
			}
		}
		System.out.println(ans);
	}
}

答案:1994240

八、缩位求和(找规律)

在这里插入图片描述
把数字的每位加起来,如果能够被9整除,那结果就是9,否则结果就是结果对9取余。

九、积木大赛(贪心)

在这里插入图片描述

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

public class Main {
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int[] num = new int[n];
		for (int i = 0; i < n; i++) {
			num[i] = scanner.nextInt();
		}
		int ans = num[0];
		for (int i = 1; i < n; i++) {
			if (num[i] > num[i - 1]) ans += num[i] - num[i - 1];
		}
		System.out.println(ans);
	}
}

※十、发现环(并查集 + 环存储)

在这里插入图片描述
在这里插入图片描述
先用并查集找到环的起点和终点,怎么找呢?用并查集进行union的时候,如果connected了,那此时的两个节点就是一个起点一个终点(题目保证只有一个环),然后用环的起点去dfs搜索,记录子节点的父节点(因为我们需要从环终点推到环起点,(环起点推到环终点并不容易,但是环终点到环起点却可以很容易找到))

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

public class Main {
	static int start, end;
	static boolean[] vis;
	static int[] path;
	static LinkedList<Integer>[] graph;
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int n = in.nextInt();
		vis = new boolean[n + 1];
		graph = new LinkedList[n + 1];
		path = new int[n + 1];
		for (int i = 0; i < n + 1; i++) {
			graph[i] = new LinkedList<>();
		}
		UF uf = new UF(n + 1);
		// 1-n
		start = -1;
		end = -1;
		while (n-- > 0) {
			int a = in.nextInt();
			int b = in.nextInt();
			if (uf.connected(a, b)) {
				// 如果a b相连了,说明a b之间有环,则这个环的起点终点就是这两个节点
				// 题目保证了只有一个环
				start = a;
				end = b;
			} else {
				uf.union(a, b);
			}
			graph[a].add(b);
			graph[b].add(a);
		}
		vis[start] = true;
		dfs(start);
		// 输出答案
		LinkedList<Integer> ans = new LinkedList<>();
		int tmp = end;
		ans.add(end);
		// 从环终点推到环起点(不要从环起点开始推!)
		while (path[tmp] != 0) {
			ans.add(path[tmp]);
			tmp = path[tmp];
		}
		Collections.sort(ans);
		for (int cur : ans) {
			System.out.print(cur + " ");
		}
	}
	static void dfs(int u) {
		for (int v : graph[u]) {
			if (vis[v]) continue;
			vis[v] = true;
			// 记录路径
			path[v] = u;
			// 找到了环终点就不要再找了
			if (v == end) {
				return;
			}
			dfs(v);
		}
	}
}
class UF {
	int count;
	int[] size;
	int[] parent;
	
	UF(int n) {
		this.count = n;
		this.parent = new int[n];
		this.size = new int[n];
		for (int i = 0; i < n; i++) {
			parent[i] = i;
			size[i] = 1;
		}
	}
	
	int find(int x) {
		while (x != parent[x]) {
			parent[x] = parent[parent[x]];
			x = parent[x];
		}
		return x;
	}
	
	boolean connected(int p, int q) {
		return find(q) == find(p);
	}
	
	void union(int p, int q) {
		int rootP = find(p);
		int rootQ = find(q);
		if (rootP == rootQ) return;
		if (size[rootP] > size[rootQ]) {
			// 小树接在大树后面
			parent[rootQ] = rootP;
			size[rootP] += size[rootQ];
		} else {
			parent[rootP] = rootQ;
			size[rootQ] += size[rootP];
		}
		count--;
	}
	
	int count() {
		return count;
	}
	
}

十一、数列求值(数学推导)

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
重在一个数学公式的推导,用到了数列中的很多知识。

十二、公约数(数论)

问题描述

给定正整数 a, b, c,请问有多少个正整数,是其中至少两个数的约数。

输入格式

输入一行包含三个正整数 a, b, c。

输出格式

输出一行包含一个整数,表示答案。

样例输入

30 70 35

样例输出

6

样例说明

1、2、5、7、10、35满足条件。

评测用例规模与约定

对于 50% 的评测用例,1 <= a, b, c <= 1000000。
对于所有评测用例,a, b, c 不超过 10**12(10的12次方)。

找数,使得该数是a b 或 a c 或 b c的公约数,这里需要注意的是,两个数的公约数一定是这两个数的最大公约数(GCD)的约数,eg:24 与 8,gcd=8,24和8的公约数有1、2、4、8,都是gcd的约数。

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

public class Main {
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    static Set<Long> set = new HashSet<>();  // 方便去重
    public static void main(String[] args) throws IOException {
    	String[] input = reader.readLine().trim().split(" ");
    	long a = Long.parseLong(input[0]);
    	long b = Long.parseLong(input[1]);
    	long c = Long.parseLong(input[2]);
    	solve(a, b);
    	solve(b, c);
    	solve(a, c);
    	System.out.println(set.size());
    }
    static long gcd(long a, long b) {
    	return b == 0 ? a : gcd(b, a % b);
    }
    static void solve(long a, long b) {
    	long d = gcd(a, b);
    	// 两个数的公约数一定是两个数的gcd的约数
    	for (long i = 1; i * i <= d; i++) {  // 只遍历到sqrt(d)
    		if (d % i == 0) {
    			set.add(i);
    			// 8 % 2 == 0, 8 / 2 = 4, 4 != 2, 所以4也算入约数
    			if (i != d / i) set.add(d / i);  // 快速找到另一半约数
    		}
    	}
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@u@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值