一. 分治法
分治问题由“分”(divide)和“治”(conquer)两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。如归并排序。
另外,自上而下的分治可以和 memoization 结合,避免重复遍历相同的子问题。如果方便推
导,也可以换用自下而上的动态规划方法求解。
例题 241. 为运算表达式设计优先级。给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
- 把加括号的过程转化成,针对每个运算符,考虑其两边的计算结果,再加入该计算符;
- 分治的主要思想是,将整体拆分成不可再分的项(这里就是不包含运算符),再组合起来;
- memoization 即可定一个map用于存储某个expression的计算结果,如果map中包含该key,则直接返回结果;
- 还可以直接使用动态规划,dp[i][j] 表示从 i 到 j 的字符串的计算结果(结果是一个list),需要考虑遍历顺序。
public List<Integer> diffWaysToCompute(String expression) {
char[] expArr = expression.toCharArray();
List<Integer> oneArr = new ArrayList<>();
List<Integer> twoArr = new ArrayList<>();
List<Integer> resArr = new ArrayList<>();
for (int i = 0; i < expArr.length; i++) {
if (expArr[i] == '+' || expArr[i] == '-' || expArr[i] == '*') {
//开始递归,分别计算运算符左右两边的结果,返回值存储在list中
oneArr = diffWaysToCompute(expression.substring(0, i));
twoArr = diffWaysToCompute(expression.substring(i+1));
//对于左右两边的结果需要混合起来计算结果
for (Integer one : oneArr) {
for (Integer two : twoArr) {
if (expArr[i] == '+') {
resArr.add(one + two);
} else if (expArr[i] == '-') {
resArr.add(one - two);
} else {
resArr.add(one * two);
}
}
}
}
}
//如果不含操作符
if (resArr.size() == 0) {
resArr.add(Integer.valueOf(expression));
}
return resArr;
}
例题 932,漂亮数组。对于某些固定的 N,如果数组 A 是整数 1, 2, …, N 组成的排列,使得:对于每个 i < j,都不存在 k 满足 i < k < j 使得 A[k] * 2 = A[i] + A[j]。那么数组 A 是漂亮数组。给定 N,返回任意漂亮数组 A(保证存在一个)。
- 这道题推导看不懂,直接每次把数组中偶数位的元素后移即可,直到数组中的元素不可再拆分。如 1 2 3 4 5 ——》1 3 5 2 4 ——》1 5 3 2 4.
public int[] mySort(int[] arr) {
int len = arr.length;
if (len <= 2) {
return arr;
}
int[] left = new int[(int)Math.ceil(len/2.0)]; //上取整
int[] right = new int[len/2]; //向下取整
int i = 0;
int j = 0;
while (i < len) {
left[j++] = arr[i];
i += 2;
}
i = 1;
j = 0;
while (i < len) {
right[j++] = arr[i];
i += 2;
}
int[] res = new int[len];
left = mySort(left); //不断拆分成更小的数组
right = mySort(right);
//下面是合并过程
for (i = 0; i < (int)Math.ceil(len/2.0); i++) {
res[i] = left[i];
}
for (j = 0; j < len/2; j++) {
res[i++] = right[j];
}
return res;
}
public int[] beautifulArray(int n) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i+1;
}
return mySort(arr);
}
二. 数学问题
1. 最大公约数/最小公倍数
求两个数的最大公约数(gcd)可以用辗转相除法,公式为 gcd(a,b) = gcd(b,a mod b):
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
最小公倍数等于两个数相乘,再除它们的最大公约数:
ind lcm(int a, int b) {
return a * b / gcd(a, b);
}
2 .找质数
例题 204,计算质数。给定整数 n ,返回所有小于非负整数 n 的质数的数量 。
- 暴力搜索超时;
- 在顺序遍历的情况下,如果找到一个质数后,将其所有倍数都排除,那么剩下的数都为质数。
public int countPrimes(int n) {
boolean[] arr = new boolean[n];
int count = 0;
for (int i = 2; i < n; i++) {
if (!arr[i]) { //因为初始化为false,因此这里要用!
count ++;
for (int j = i+i; j < n; j=j+i) {
arr[j] = true;
}
}
}
return count;
}
3. 进制转换
例题504,七进制数。给定一个整数 num,将其转化为 7 进制,并以字符串形式输出。
- 进制转化的题需要用到相除和取模;
- 拿几个例子分析一下就会发现规律。
public String convertToBase7(int num) {
if(num == 0) {
return "0";
}
boolean isNegative = false;
if (num < 0) {
isNegative = true;
num = -num;
}
int a = 0;
int b = 0;
String res = "";
while (num != 0) {
a = num / 7;
b = num % 7;
res = String.valueOf(b) + res;
num = a;
}
return isNegative ? "-" + res : res;
}
例题 168, Excel表列名称。给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。如:
A -> 1
B -> 2
C -> 3
…
Z -> 26
AA -> 27
AB -> 28
…
- 这道题是7进制转换的变种题,难点在于是从1开始(A对应1),所以要注意columnNumber-1;
- 另外在于要 append char类型的字符,toString() 才能得到正确的结果。
public String convertToTitle(int columnNumber) {
int m = 0;
int n = 0;
StringBuilder res = new StringBuilder();
while(columnNumber != 0) {
m = (columnNumber-1) / 26;
n = (columnNumber-1) % 26;
columnNumber = m;
res.append((char)(n + 'A'));
}
res.reverse();
return res.toString();
}
4. 阶乘后的零
例题 172,阶乘后的零。给定一个整数 n ,返回 n! 结果中尾随零的数量。
- 暴力解法必溢出;
- 分析题目,尾部含0必定是一个数乘了10,而10必是由一对2*5得到,因此只需要计算阶乘中各个因子2和5的个数;
- 由于2每2个数出现一次,5每5五个数出现一次,因此5的个数一定大于2,只需要计算5的个数即可;
- 每5个数会出现一次5,但每25个数会出现2个5,125会出现3个5……
- 因此5的个数最终为 n / 5 + n / 25 + n / 125 + ……
public int trailingZeroes(int n) {
int count = 0;
while(n > 0) {
count += n / 5; //每隔5个数会出现一个5
n = n / 5; //while循环是因为每隔25又会多出现一个5,
}
return count;
}
5. 字符串相加
例题 415,字符串相加。给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
- 注意两个整数长度不一致的问题,注意进位问题;
- 可以用两个指针分别指向整数的最后一位进行相加操作,如果有一个整数长度不够,则补零进行运算;
public String addStrings(String num1, String num2) {
StringBuilder res = new StringBuilder();
int i = num1.length() - 1;
int j = num2.length() - 1;
int cent = 0;
int tmp = 0;
while (i >=0 || j >= 0) {
//补零解决长度不一的问题
int a = i >= 0 ? num1.charAt(i) - '0' : 0;
int b = j >= 0 ? num2.charAt(j) - '0' : 0;
tmp = a + b + cent;
cent = tmp / 10;
tmp = tmp % 10;
res.append(tmp + "");
i--;
j--;
}
if (cent == 1) {
res.append("1");
}
return res.reverse().toString();
}
6. 3 的幂
例题 326,3 的幂。给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。
public boolean isPowerOfThree(int n) {
if (n==0) {
return false;
}
while (n % 3 == 0) {
n = n / 3;
}
if (n == 1) {
return true;
}
return false;
}
7. 按权重随机选择
例题 528. 给你一个 下标从 0 开始 的正整数数组 w ,其中 w[i] 代表第 i 个下标的权重。请你实现一个函数 pickIndex ,它可以 随机地 从范围 [0, w.length - 1] 内(含 0 和 w.length - 1)选出并返回一个下标。选取下标 i 的 概率 为 w[i] / sum(w) 。
- 按权重选择的关键在于求数组的累计和,然后取一个随机数,随机数在 [1, total] 之间,随机数落在哪个区间即返回哪个下标;
- 由于累计和数组是有序的,因此可以通过二分法加速。
class Solution {
//存储w累计和
private int[] sumw;
public Solution(int[] w) {
sumw = new int[w.length];
sumw[0] = w[0];
for(int i = 1; i < w.length; i++) {
sumw[i] = sumw[i-1] + w[i];
}
}
public int pickIndex() {
Random random = new Random();
int num = random.nextInt(sumw[sumw.length-1]) + 1;
//num落在哪个区域即返回哪个下标
//使用二分法进行优化
int left = 0;
int right = sumw.length-1;
while (left < right) {
int key = left + (right - left) / 2;
if (num <= sumw[key]) {
right = key;
} else {
left = key + 1;
}
}
return left;
}
}
8. 链表随机选择
例题382,链表随机选择。给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 被选中的概率一样 。
实现 Solution 类:
Solution(ListNode head) 使用整数数组初始化对象。
int getRandom() 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。
- 链表与数组的不同在于链表不知道长度,可以在初始化的时候先遍历一遍,存入数组,再取数;这里考虑一种水库算法。
- 水库算法:遍历一次链表,在遍历到第 m 个节点时,有 1/m 的概率选择这个节点覆盖掉之前的节点选择,(m-1)/m 的概率保留原来的数,而原来的数出现的概率可以证明是1/m-1,因此每个数出现的该概率都会是1/m。
class Solution {
private ListNode node;
private Random random;
public Solution(ListNode head) {
node = head;
random = new Random();
}
public int getRandom() {
int count = 1; //统计总节点个数
int reserve = 0; //存储当前被保留的数
ListNode cur = node;
while(cur != null) {
int num = random.nextInt(count) + 1;
//if表示选择当前节点的概率为1/count
//否则不选择该节点的概率是(count-1)/count
if (num == count) {
reserve = cur.val; //选择当前数覆盖之前的选择
}
cur = cur.next;
count++;
}
return reserve;
}
}
9. 其他
例题 238. 除自身以外数组的乘积。 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。请不要使用除法,且在 O(n) 时间复杂度内完成此题。
- 两次for循环分别计算左边的乘积和右边的乘积,再相乘
/**
两次for循环分别计算左边的乘积和右边的乘积,再相乘
*/
public int[] productExceptSelf(int[] nums) {
int[] res = new int[nums.length];
int mul = 1; //存储当前元素左边的乘积
for (int i = 0; i < nums.length; i++) {
res[i] = mul;
mul = mul * nums[i];
}
mul = 1; //存储当前元素右边的乘积
for (int i = nums.length-1; i >=0 ; i--) {
res[i] = res[i] * mul; //等号右边的res[i]存储左边的乘积,mul存储右边的乘积
mul = mul * nums[i];
}
return res;
}
例题 169, 多数元素。给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
- 排序法
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
- 摩尔投票法。初始化候选人为第一个元素,选票为1,如果下一个数与之相等,选票加一,否则选票减一。如果选票等于0,则将候选人更新为当前元素,并且将选票初始化为1。遍历完数组后即得到多数元素。
- 原理是因为多数元素个数之和一定会大于其他数个数之和,所以遍历完数组之后,多数元素起码比其他元素多出一个。
public int majorityElement(int[] nums) {
int res = nums[0];
int count = 1;
for (int i = 1; i < nums.length; i++) {
if (res == nums[i]) {
count ++;
} else {
count --;
}
if (count == 0) {
res = nums[i];
count = 1;
}
}
return res;
}
例题 470,用 Rand7() 实现 Rand10()。给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。
- 原理:用 (randX() - 1) * Y + randY() 可以生成均匀的 [1, X*Y] 的数(可以代入两个数算算看看)
- 因此,(rand7() - 1) * 7 + rand7() 可以生成均匀的 [1, 49] 的数,舍弃掉超过 40 的数,则可以得到均匀的 [1, 40] 的数,即也可以得到 [1, 10] 的数;Y 还可以取 2、3…… 都可以,只是这样还得先构造 rand2() 和 rand3()。
class Solution extends SolBase {
public int rand10() {
int num = 0;
while(true) {
num = (rand7() - 1) * 7 + rand7();
if (num <= 40) {
break;
}
}
return (num-1) / 4 + 1;
}
}
例题202,快乐数。编写一个算法来判断一个数 n 是不是快乐数。「快乐数」 定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果这个过程 结果为 1,那么这个数就是快乐数。如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
- 依题意,若存在循环则不可能是快乐数;找循环可以想到找环;而找环可以想到用快慢指针。
public int square(int n) {
int res = 0;
while (n > 0) {
res += (n % 10) * (n % 10);
n = n / 10;
}
return res;
}
public boolean isHappy(int n) {
//快慢指针判断是否存在环
int fast = n;
int slow = n;
do {
fast = square(fast);
fast = square(fast);
slow = square(slow);
} while (slow != fast);
return fast == 1;
}
本文深入探讨了分治法在解决计算表达式和数组排序问题中的应用,以及数学问题在编程竞赛中的常见解题策略,包括最大公约数、质数、进制转换、阶乘后的零、字符串相加、3的幂、按权重随机选择和链表随机选择等。此外,还介绍了如何利用这些方法解决实际编程挑战,如Excel列名转换、快速随机选择和快乐数判断等。
1384

被折叠的 条评论
为什么被折叠?



