算法学习
特殊函数
-
海伦公式
p = (a+b+c)/ 2
S = sqrt(p * (p-a) * (p-b) * (p-c)) -
排列组合
Cnm = Anm / Amm = n! / m!(n-m)! / n * (n-1)…(n-m+1) / m!
Anm = n * (n-1)…(n-m+1) / n! / (n-m)! -
二叉树
数组表示树:2i+1,2i+2左右子树,父结点 (i-1)/ 2
Java
-
== 如果在[-128,127]会被cache缓存,比较值,超过这个范围则比较的是对象是否相同
-
Map.getOrDefault(Object key, V defaultValue) 方法的作用是:当Map集合中有这个key时,就使用这个key对应的值;如果没有就使用默认值defaultValue。
-
Arrays.stream(nums).distinct().count() :去重统计不同个数
-
String.getBytes():获取字符串中每一个字符的ASCII的值
-
Integer.toBinaryString(int):将整数转化为二进制字符串
-
String类:使用replace()函数是返回一个替换后的字符串,而字符串本身并未改变。因此需要将返回值赋予自身变量,例如str = str.replace(oldChar,newChar)。
-
StringBuilder型字符串无法直接使用equals()比较两个字符串是否相等,因为StringBuilder类的equals()没有重写,内部依然使用了**==的比较方法,所以即使两个相等的字符串比较也为false**。因此需要将两个StringBuilder类型字符串用toString()转换为String类型后再用equals()进行比较。
-
StringBuilder类的reverse()方法是对原对象进行翻转
-
StringBuilder比StringBuffer效率高但线程不安全
-
在 Java 中,我们通过取小数部分(利用 % 1)来检查数字是否是整数,并检查它是否是 0。
-
String.format("%4d", num) : 右对齐占4位
System.out.printf("%-10s",“abc”); //输出10列,左对齐(-号表示左对齐)
System.out.printf("%8d",23); //输出8列, 右对齐
%10s : 使字符串右对齐输出,不足10位,前加空格。
System.out.printf("%d\n%d\n%.2f", max, min, (double)sum / n); -
求二进制位数
(int) Math.ceil(Math.log(num) / Math.log(2)) -
PriorityQueue<int[]> minHeap = new PriorityQueue<>(Comparator.comparingInt(a -> a[1])); 优先队列,重写比较器
static Comparator comparingInt(ToIntFunction<? super T>
keyExtractor)
接受提取的一个函数int从类型分类键T ,并返回一个Comparator ,通过该排序关键字进行比较。
如果指定的功能也可串行化,则返回的比较器是可序列化的。
- Collections.sort(list,Comparator.comparingInt(Order::getValue));
- sum(i%2==int(v) for i, v in enumerate(s))(生成交替二进制字符串的最少操作数)
Python
- dict = collections.Counter(str) :返回一个统计不同字符数量的字典对象
- dict.values():返回所有值组成的列表
- dict.items():返回键值对元组组成的列表
- chr():将ASII码转为字符
- ord():将字符转为ASCII码
- ’{0:b}’.format(int(a, 2) + int(b, 2)):将整数转化为二进制数字符串表示,int(str, 2) :将字符串转化为指定进制整数。
- [0] * num : 初始化num个0的列表
- list(range(1, 10 ** n)):range()方法生成可迭代对象,在使用list()转为列表
- 快速生成列表
dp = [arr[i][1] for i in range(n)]
- sorted():返回一个新的已排序的列表,参数 key = str.lower(每个元素中提取用于比较的键), reverse = True:反向进行排序
也可以指定关键字排序:arr.sort(key=lambda x : (x[0], x[1]))
- reduce(gcd, vals):将两个参数的 function 从左至右积累地应用到 iterable 的条目,以便将该可迭代对象缩减为单一的值。 例如,reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) 是计算 ((((1+2)+3)+4)+5) 的值。 左边的参数 x 是积累值而右边的参数 y 则是来自 iterable 的更新值。
- gcd(x, y):求最大公约数
- 两个列表相加相当于列表合并
- itertools.product(*iterables, repeat=1):
可迭代对象输入的笛卡儿积。
大致相当于生成器表达式中的嵌套循环。例如, product(A, B) 和 ((x,y) for x in A for y in B) 返回结果一样。
嵌套循环像里程表那样循环变动,每次迭代时将最右侧的元素向后迭代。这种模式形成了一种字典序,因此如果输入的可迭代对象是已排序的,笛卡尔积元组依次序发出。
要计算可迭代对象自身的笛卡尔积,将可选参数 repeat 设定为要重复的次数。例如,product(A, repeat=4) 和 product(A, A, A, A) 是一样的。
list(product(range(3),repeat=1))
[(0,), (1,), (2,)]
-
itertools.combinations(iterable, r)
返回由输入 iterable 中元素组成长度为 r 的子序列。
组合元组会以字典顺序根据所输入 iterable 的顺序发出。 因此,如果所输入 iterable 是已排序的,组合元组也将按已排序的顺序生成。
即使元素的值相同,不同位置的元素也被认为是不同的。如果元素各自不同,那么每个组合中没有重复元素。 -
any(iterable)
如果 iterable 的任一元素为真值则返回 True。 如果可迭代对象为空,返回 False。 等价于:
def any(iterable):
for element in iterable:
if element:
return True
return False
- zip(*iterables)
创建一个聚合了来自每个可迭代对象中的元素的迭代器。
返回一个元组的迭代器,其中的第 i 个元组包含来自每个参数序列或可迭代对象的第 i 个元素。
当所输入可迭代对象中最短的一个被耗尽时,迭代器将停止迭代。 当只有一个可迭代对象参数时,它将返回一个单元组的迭代器。
不带参数时,它将返回一个空迭代器。 相当于:
zip():将列表对应位置的每个元素,压缩为一个元组。
zip(*):解压缩
zip() 与 * 运算符相结合可以用来拆解一个列表:
>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> zipped = zip(x, y)
>>> list(zipped)
[(1, 4), (2, 5), (3, 6)]
>>> x2, y2 = zip(*zip(x, y))
>>> x == list(x2) and y == list(y2)
True
例如:
A = ["cba","daf","ghi"]
zip(*A):["cdg", "bah", "afg"]
ans = 0
for col in zip(*A):
if any(col[i] > col[i+1] for i in range(len(col)-1)):
ans += 1
return ans
- 计算椭圆相交面积
from shapely.geometry import Polygon
p1=Polygon([(0,0),(1,1),(1,0)])
p2=Polygon([(0,1),(1,0),(1,1)])
print p1.intersects(p2)
x = p1.intersection(p2)
x.area # 从相交处生成的多边形
典型例题
判断回文数
算法思想:
一、set集合:回文串关于中心对称。其特点是除了长度为奇数的回文串中间的那个字符,其余字符若在前边出现一次,则必然在后边也出现一次。所以某个字符串若为回文串的一个排列,则其中至多有一个孤立的字符,其余字符应两两成对。
据此,可创建一个空集合,然后从头到尾扫描字符串。若扫描到的字符不在集合中,将其放入集合;若扫描到的字符在集合中,则可与其配成一对,为此从集合中移除该字符。
当扫描完字符串时,若集合中元素个数小于2(也就是集合为空或集合中只有一个元素),则该字符串中的所有字符可以构成一个回文串,也就是该字符串是回文串的一个排列。反之,则不是。
def canPermutePalindrome(self, s: str) -> bool:
myset = set()
for ch in s:
if ch in myset:
myset.remove(ch)
else:
myset.add(ch)
return len(myset) < 2
二、位运算:出现重复的字符(转化为1作业多少位),异或之后为0,最后可能只剩一个字符,若该字符是移位后是2的n次方即是回文数
def canPermutePalindrome(self, s: str) -> bool:
result = 0
for c in s:
result ^= 1<<ord(c)
return (result & (result - 1)) == 0
x的平方
换底公式:x ^ 2 = e ^ 1/2 * lnx
二分查找:由于x的平方根整数部分ans满足k^2<=x的最大值k,因此对k进行二分查找
二进制求和
把 aa 和 bb 转换成整型数字 xx 和 yy,在接下来的过程中,xx 保存结果,yy 保存进位。
当进位不为 0 时:
计算当前 x 和 y 的无进位相加结果:answer = x ^ y
计算当前 x 和 y 的进位:carry = (x & y) << 1
完成本次循环,更新 x = answer,y = carry
返回 x 的二进制形式
为什么这个方法是可行的呢?
在第一轮计算中,answer 的最后一位是 x 和 y 相加之后的结果,carry 的倒数第二位是 x 和 y 最后一位相加的进位。
接着每一轮中,由于 carry 是由 x 和 y 按位与并且左移得到的,那么最后会补零,所以在下面计算的过程中后面的数位不受影响,
而每一轮都可以得到一个低 i 位的答案和它向低 i + 1 位的进位,也就模拟了加法的过程。
def addBinary(self, a, b) -> str:
x, y = int(a, 2), int(b, 2)
while y:
answer = x ^ y
carry = (x & y) << 1
x, y = answer, carry
return bin(x)[2:]
汉诺塔
1~N 从 A 移动到 B,C 作为辅助
等价于:
1,1~N-1 从 A 移动到 C,B 为辅助
2,把 N 从 A 移动到 B
3,1~N-1 从 C 移动到 B,A 为辅助
二分查找
全范围内二分查找
等价于三个问题
左边比(递归)
中间比
右边找(递归)
int mid = (low+high) >>> 1; // 防止溢出,移位也更高效
希尔排序
-
思路:如序列 9 8 7 6 5 4 3 2 1
确定一个增量序列,如 4(length / 2) 2 1,从大到小使用增量 -
使用第一个增量将序列划分若干子序列,下标组合为 0-4-8, 1-5,2-6,3-7
-
依次对子序列使用直接插入排序
-
使用第二个增量,将序列划分为若干子序列(0-2-4-6-8),(1-3-5-7)
-
依次使用直接插入排序
-
使用第三个增量1,这时子序列就是原序列
时间复杂度:O(nlogn)~O(n2)
空间复杂度: O(1)
原址排序
稳定性: 由于相同元素可能划分至不同子序列单独排序,因此稳定性无法保证–不稳定
棋盘覆盖
问题: 在一个2 ^ k * 2 ^ k个方格组成的棋盘,恰有一个方格与其他方格不同,称该方格为特殊方格。显然该特殊方格在棋盘上出现共有4 ^ k种情形。在该特殊棋盘上需要用L型骨牌将除了特殊方格外的其他方格全部覆盖,且任何两个骨牌不能重叠,在一个期盼中,骨牌个数为 (4 ^ k-1) / 3。
分析: 当k>0时,主要是将一个2 ^ k * 2 ^ k的棋盘分为2 ^ k-1 * 2 ^ k -1的棋盘,将大问题递归为小问题。其中特殊方格必位于其中一个小方格,其余三个子棋盘无特殊方格,为了将这三个棋盘转化为子棋盘,可以用一个L型骨牌覆盖这个三个棋盘的汇合处。将棋盘化为2*2的时候便可以解决问题。
/**
*
* @param tr: 棋盘左上角方格行号
* @param tc: 左上角列号
* @param dr: 特殊方格所在行号
* @param dc: 列号
* @param size:棋盘规模size*size
*/
public static void chess_board(int tr, int tc, int dr, int dc, int size) {
if (size == 1)
return;
int t = tile++; // L型骨牌牌号
int s = size / 2; // 求解子问题
// 覆盖左上角子棋盘
if (dr < tr + s && dc < tc + s) {
// 特殊方格 在此棋盘中, 继续缩小子问题
chess_board(tr, tc, dr, dc, s);
} else { // 此棋盘没有特殊方格
// 用t号L型骨牌覆盖右下角
board[tr + s - 1][tc + s - 1] = t;
// 覆盖其余方格
chess_board(tr, tc, tr + s - 1, tc + s - 1, s);
}
// 覆盖右上角子棋盘
if (dr < tr + s && dc >= tc + s) {
// 特殊方格 在此棋盘中, 继续缩小子问题
chess_board(tr, tc + s, dr, dc, s);
} else { // 此棋盘没有特殊方格
// 用t号L型骨牌覆盖右下角
board[tr + s - 1][tc + s] = t;
// 覆盖其余方格
chess_board(tr, tc + s, tr + s - 1, tc + s, s);
}
// 覆盖左下角子棋盘
if (dr >= tr + s && dc < tc + s) {
// 特殊方格 在此棋盘中, 继续缩小子问题
chess_board(tr + s, tc, dr, dc, s);
} else { // 此棋盘没有特殊方格
// 用t号L型骨牌覆盖右上角
board[tr + s][tc + s - 1] = t;
// 覆盖其余方格
chess_board(tr + s, tc, tr + s, tc + s - 1, s);
}
// 覆盖右下角子棋盘
if (dr >= tr + s && dc >= tc + s) {
// 特殊方格 在此棋盘中, 继续缩小子问题
chess_board(tr + s, tc + s, dr, dc, s);
} else { // 此棋盘没有特殊方格
// 用t号L型骨牌覆盖左上角
board[tr + s][tc + s] = t;
// 覆盖其余方格
chess_board(tr + s, tc + s, tr + s, tc + s, s);
}
}
完美素数
对于一个 正整数,如果它和除了它自身以外的所有 正因子 之和相等,我们称它为 「完美数」。
解法一:
枚举:从1到sqrt(num),枚举即可。这是因为如果num有一个大于sqrt(n)的因数,那么一定有一个小于sqrt(num)的因数n/x,特殊情况是x = n/x;
解法二:
欧几里得-欧拉定理告诉我们,每个偶完全数都可以写成2^(p-1) * (2 ^ p - 1)的形式,其中 p为素数。例如前四个完全数可以写成如下形式:
6 = 2^1 * (2^2 - 1)
28 = 2^2 * (2^3 - 1)
496 = 2^3 * (2^4 - 1)
8128 = 2^4 * (2^5 - 1)
由于目前奇完全数还未被发现,因此所有的完全数都可以写成上述形式。当 n 不超过 10^8 时,p 也不会很大,因此我们只要带入最小的若干个素数 2, 3, 5, 7, 13, 17, 19, 31,将不超过 10 ^ 8 的所有完全数计算出来即可。
2 ^ n 相当于 1 << n 位
public boolean checkPerfectNumber(int num) {
int[] primes=new int[]{2,3,5,7,13,17,19,31};
for (int prime: primes) {
if ((1 << (prime-1))* ((1 << prime) - 1) == num)
return true;
}
return false;
}
十进制整数的反码
- 解法一:
原理:原码+反码 = 2^n-1 n为二进制位数
循环条件:num <= N,即可求出2^n
if(N==0)
return 1;
int num = 1;
while(num <= N) {
num <<= 1;
}
return num-1-N;
- 解法二:
2^n-1 = N
n = log2^N+1;
n为长度
由于Math类中无log2,默认log以e为底,log10以10为底,因此使用换底公式log2^x=log e ^ x / log e ^ 2;
public int bitwiseComplement(int N) {
if (N == 0)
return 1;
int length = (int)(Math.log(N) / Math.log(2)) + 1;
return (int)Math.pow(2, length) - 1 - N;
}
- 解法三:
根据反码的定义 等于原码取反
原码与对应二进制位数的全与1异或就是反码
public int bitwiseComplement(int N) {
int num=1;
while(num<N) {
num = (num<<1)+1;
}
return num ^ N;
}
小白上楼梯
爬楼梯一次可以爬一步,两步,三步,问爬到n阶台阶的方法
static int f(int n) {
if (n == 0) return 1;
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1)+f(n-2)+f(n-3);
}
小白上楼梯2
爬楼梯每次至少爬d阶,爬到n阶台阶的方案数
动态规划,每次向前推d,第i-d的阶数方案会对下一次造成影响。
int sum = 1;
int mod = 1000000007;
System.out.println(mod);
for (int i = d; i <= n; i++) {
sum += x[i-d];
x[i] += sum;
System.out.print(x[i]+" ");
x[i] %= mod;
sum %= mod;
}
System.out.println(x[n]);
最大字段和
给定序列,找出最大的子段和
累加每个元素,当sum<0,将其初始化为0
long sum = 0, max = 0;
for (int i = 0; i < n; i++) {
sum += x[i];
max = Math.max(max, sum);
if (sum < 0)
sum = 0;
}
System.out.println(max);
最长公共子串
在字符串中任意个连续的字符组成的子序列成为该串的子串,给定两个字符串,求出最长的公共子串的长度
思想:从头遍历一个字符串,当字符串不包含则将指针++
String s = sc.nextLine();
String x = sc.nextLine();
int max = 0, i = 0;
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
sb.append(c);
String ss = sb.substring(i);
if (x.contains(ss)) {
max = Math.max(max, ss.length());
} else {
i++;
}
}
System.out.println(max);
最长公共子序列
-
最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢? 即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢 给定串中任意个连续的字符组成的子序列称为该串的子串。
-
举个例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),
- 假如S1的最后一个元素与S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
- 假如S1的最后一个元素与S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于: {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。
int[] x = {0, 2, 5, 7, 3, 6, 8, 4};
int[] y = {0, 3, 4, 7, 3, 6, 4};
int[][] xx = new int[x.length+1][y.length+1];
for (int i = 1; i < x.length; i++) {
for (int j = 1; j < y.length; j++) {
if (x[i] == y[j])
xx[i][j] = xx[i-1][j-1]+1;
else
xx[i][j] = Math.max(xx[i-1][j], xx[i][j-1]);
}
}
System.out.println(xx[x.length-1][y.length-1]);
- 输出最长公共子序列
类似的倒推回去:当x[n] i= y[n]时,比较xx[i-1][j]与x[i][j-1]如果若大于就选i-1与j反之选择i与j-1,当等于时选择不同的方向有不同的结果。
int i = x.length-1, j = y.length-1;
StringBuilder sb = new StringBuilder();
while (xx[i][j] > 0) {
if (x[i] == y[j]) {
sb.append(x[i]);
i--;
j--;
} else if (x[i] != y[j]) {
if (xx[i - 1][j] > xx[i][j - 1]) {
i--;
} else {
j--;
}
}
}
System.out.println(sb.reverse().toString());
最长递增子序列
- DP-动态规划
状态设计:用一个维护数组dp[i]表示以a[i]结尾的最长递增子序列的长度
状态转移:之后向前找到一个小于a[i]的进行状态转移dp[i] = Math.max(dp[i], dp[j]+1);
边界处理:dp[i]=1(0<=j<n)
时间复杂度:O(n2)
int[] x = {3, 1, 2, 1, 8, 5};
// dp[i]表示以a[i]结尾的最长递增子序列的长度
dp = new int[x.length];
int ans = 0;
for (int i = 0; i < x.length; i++) {
// 初始化每一个dp[i]=1
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (x[i] > x[j])
dp[i] = Math.max(dp[i], dp[j]+1); // 状态转移
}
ans = Math.max(ans, dp[i]); // 比较每一个dp,取最大值
}
System.out.println(ans);
- 贪心+二分查找
利用贪心的思想,对于一个上升的子序列,显然当前最后一个元素越小,越有利于添加新的元素,这样LIS长度自然更长。
因此,我们只需要维护dp数组,其表示就是长度为i+1的LIS结尾元素的最小值,保证每一位都是最小值
dp数组单调递增,因此对于每一个a[i],先判断是否可以直接插入到dp数组尾部,即比较其与dp数组的最大值,即最后一位;如果不可以则找出dp数组中第一个大于等于a[i]的位置,用a[i]替换之。
int[] dp2 = new int[x.length];
int index = 0;
dp2[0] = x[0];
for (int i = 1; i < x.length; i++) {
if (x[i] > dp2[index])
dp2[++index] = x[i];
else
dp2[bin(index, x[i])] = x[i];
}
System.out.println(index+1);
// 二分查找
public static int bin(int e, int x) {
int s = 0;
while (s < e) {
int mid = (s + e) >>> 1;
if (dp[mid] < x)
s = mid+1;
else
e = mid-1;
}
return s;
数组中的最小差值
给你一个整数数组,给每一个元素加上任意一个数组x (-K <= x <= K),从而得到新数组,返回数组中最大值与最小值的最小差值
解析:
最小化max(B) - min(B)也就是分别最小化max(B)和最大化min(B)。max(B)最小可能为max(A)-K,min(B)最大化是min(A)+K所以最小就是max(A) - min(A) - 2*K;
范围求和
给定一个初始元素全部为 0,大小为 m*n 的矩阵 M 以及在 M 上的一系列更新操作。
操作用二维数组表示,其中的每个操作用一个含有两个正整数 a 和 b 的数组表示,含义是将所有符合 0 <= i < a 以及 0 <= j < b 的元素 M[i][j] 的值都增加 1。
在执行给定的一系列操作后,你需要返回矩阵中含有最大整数的元素个数。
解法:
我们可以观察到最大元素会是两个操作对应矩阵的交集区域。我们还可以发现要求这块区域,我们不需要将操作区域一个一个加一,我们只需要记录交集区域的右下角即可。这个角的计算方法为:表示所有操作的 op[i]op[i] 中的最小值。
这样,最大元素的数目就是 x×y。
for item in ops:
m = min(m, item[0])
n = min(n, item[1])
return m * n
奇数在左,偶数在右
思路:类似快速排序,左指针搜索偶数,右指针搜索奇数,交换,结束条件是两指针交叉。
阶乘尾数
求出阶乘获得尾数0的个数
n! 的尾数0 就是 不断累加 n/5 直到 n=0为止
class Solution {
public int trailingZeroes(int n) {
int ans = 0;
while(n >= 5) {
ans += n / 5;
n /= 5;
}
return ans;
}
}
矩阵连乘
多个矩阵相乘,乘的顺序不一样,计算的次数也不同
例如:给定三个连乘矩阵{A1,A2,A3}的维数分别是10 * 100,100 * 5和5 * 50,采用(A1A2)A3,乘法次数为10*100 * 5+10 * 5 * 50=7500次,而采用A1(A2A3),乘法次数为100 * 5 * 50+10 * 100 * 50=75000次乘法,显然,最好的次序是(A1A2)A3,乘法次数为7500次。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] p = new int[n+1];
for (int i = 0 ; i <= n; i++) {
p[i] = sc.nextInt();
}
long[][] m = new long[n+1][n+1];
for(int i = 1;i <= n;i++)//初始化,矩阵长度为1时,从i到i的矩阵连乘子问题只有一个矩阵,操作次数是0
{
m[i][i] = 0;
}
//不同规模的子问题
for (int r = 2; r<= n; r++) {
//每一个规模为r的矩阵连乘序列的首矩阵Ai
for (int i = 1; i <= n-r+1; i++) { //从第i个矩阵开始,长度为r,则矩阵为(Ai-A(i+r-1))
int j = i + r - 1; //每一个规模为r的矩阵连乘序列的尾矩阵Aj
m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j]; // 决策为k=i的乘法次数
//对Ai...Aj的所有决策,求最优值,记录最优决策
for (int k = i+1; k < j; k++) {
long cur = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if (cur < m[i][j])
m[i][j] = cur;
}
}
}
System.out.println(m[1][n]);
}
}
Palindrome
求最少增加几个字符使得给定的字符串形成回文串
分析:为该串与该串的倒序的最长公共子序列
由于最长公共子序列,每次只需要前面的值,所以数组长度为2,节省空间
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
sc.nextLine();
String s = sc.nextLine();
StringBuilder sb = new StringBuilder(s);
char[] x = sb.toString().toCharArray();
char[] y = sb.reverse().toString().toCharArray();
int[][] dp = new int[2][x.length+1];
for(int i = 0; i < x.length; i++) {
for (int j = 0; j < y.length; j++) {
if (x[i] == y[j])
dp[(i+1)%2][j+1] = dp[i%2][j]+1;
else
dp[(i+1)%2][j+1] = Math.max(dp[i%2][j+1], dp[(i+1)%2][j]);
}
}
System.out.println(s.length() - dp[x.length%2][x.length]);
}
}
完全背包
有一个背包容量有限,不同的物品有不同的价值需要不同的容量,求使背包的容量的最大价值。动态规划:当前物品,可以选择加入或不加入两种选择,不加入则价值等于dp[i-1][j],加入的话则是:dp[i-1][j-w[i]]+v[i]与dp[i-1][j]的最大值。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int W = sc.nextInt();
int[] w = new int[n];
int[] v = new int[n];
for (int i = 0; i < n; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
}
int[][] dp = new int[n][W+1];
for (int i = 0; i <= W; i++) {
dp[0][i] = i/w[0]*v[0];
}
int max = 0;
for (int i = 1; i < n; i++) {
for (int j = 1; j <= W; j++) {
for (int k = 0; k*w[i] <= j; k++) {
int t = k*v[i]+dp[i-1][j-k*w[i]];
if (t > max)
max = t;
}
dp[i][j] = max;
max = 0;
}
}
System.out.println(dp[n-1][W]);
}
}
图像压缩
-
问题描述:
-
图像压缩的问题我们是这样理解的:大家都知道计算机的图像是用灰度值序列来表示的{P1,P2…Pn},其中Pi表示像素点 i的灰度值。而通常灰度值的范围是0~255,因此需要8位二进制数来表示一个像素。这个时候大家应该有了一些小的疑问:我能不能用更少的位数来表示灰度值?(因为有的灰度值并没有达到255这么大)所以我们引入了图像压缩算法来解决这个问题。
-
算法设计的知识
我们要将灰度值序列分组,而每一组中所有的数就有可能是 <255 的,所以我们就不需要用8位数字去表示像素大小了,但是分组会带来一个新的问题:我如何表示当前组中像素的个数和像素的位数呢(因为不是八位,所以要有一个数据来记录真正的位数)?这里我们引入两个固定位数的值来表示,①我们用3位数字来表示当前组的每一位像素的的位数②我们引入8来表示当前组中像素点的个数。 -
因为我们在这里规定了一组中最多存储–>0~255个数字,而一个灰度值最多有8位(2^3),所以我们可以用即3位数字来表示当前组的像素位数(注意这里都是二进制)
例如:
{6, 5, 7,5, 245, 180, 28,28,19, 22, 25,20}这是一组灰度值序列。我们按照默认的解体方法来看----一共12个数字,所以12*8=96位来表示。
而下面我们将其进行分组:
这里我们将他们分为三组:
第一组4个数,最大是7所以用3位表示;
第二组2个数,最大是245所以用8位表示;
第三组6个数,最大是28所以用5位表示;
这个时候,我们最后得到了最后的位数结果为:43+28+65+113=91。是不是优化了??
- 那我们算法应该怎么做来找最优的值呢??
既然是DP问题,所以我们肯定需要数组来记录每一步的最优值。这里我们用S[n]来记录第i个数字的最优处理方式得到的最优解。l[n] 中来记录第当前第i个数所在组中有多少个数。而b[n] 中存的数为当前组的像素位数。
在求解过程中,我们知道在我们求s[3]的时候,我们是分三种情况
①前三个数为一组,这个时候我需要的存储位数是3(位数)*3(每一组中数的个数)+11(每分一组所必须的固定位数)
②s[1]为单独一组,剩下的两个数字为一组,此时我所需要的空间为s[1]+2*3+11
③前两个数字为一组,最后一个数为一组。此时我们要用s[2](前面已经计算出的最优值)+3*1+11
然后比较三个数的大小,取最小的那一种分组情况,然后记下l[3]=3(当前最优分组中是三个数在同一组中),b[3]=3(每一个像素所用的存储位数)。
代码:
import java.util.Scanner;
public class oj1246 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] x = new int[n+1];
long[] dp = new long[n+1]; // 动态规划数组,记录第i个数字的最优处理方式
int[] b = new int[n+1]; // 代表第i个数的二进制位数
for (int i = 1; i <= n; i++) {
x[i] = sc.nextInt();
// 获取当前数的二进制位数
b[i] = (int)Math.ceil(Math.log(x[i]+1) / Math.log(2));
}
// 从第一个数开始
for (int i = 1; i <= n ; i++) {
int bmax = b[i]; // 当前二进制位数
// 初始化前i-1个数一组
dp[i] = dp[i-1] + bmax;
// 前i-j个数为一组,需要求出之间最大值的位数
// 从后往前推,首先 前i-1个数位一组,最后一个单独一组,之后前i-2个数一组,倒数后两个一组,j代表最后一组的个数
for (int j = 2; j <= i && j < 256; j++) {
if (bmax < b[i-j+1])
bmax = b[i-j+1];
if (dp[i] > dp[i-j] + j*bmax) {
dp[i] = dp[i-j] + j*bmax;
}
}
dp[i] += 11; // 求完后加11
}
System.out.println(dp[n]);
}
}
位运算
- << >> 将二进制位进行左移右移
- ‘>>>’ 右移不带符号用0填充高位,>> 运算符用符号位填充高位
- 对于int型,1<<35与1<<3是相同的,int 32 位,左边数是long型需要对右边数模64
- 与:都为1结果为1,或:有一个为1结果为1,异或:二者不同时结果1
- 判断奇偶:x&1 == 1 **?**奇数 **:**偶数
- 1<<n = 2^n,n << 1 = 2n
- 算术右移:数字向右移动,高位以符号位填充,低位越界后舍弃; == x/2向下取整
- 逻辑右移:高位以0填充
- 异或可以理解为: 1+0 = 1, 1+1=0, 0+0=0,相异为1
性质:交换律,可任意交换运算因子的位置,结果不变、结合律、对于任何x 都有 x^x = 0, x ^ 0 = x、自反性:A ^ B ^ B = A ^ 0 = A - (x-1) & x:消掉最低位的1,也可判断是否是2的n次方
- even = x&0xaaaaaaaa; // 和1010 1010 1010…做与运算求出偶数位,二进制的4位相当于16进制的1位
odd = x&0x55555555; // 取出奇数位
最接近点对
一维问题: 在一组点中,找出最近点对,该对点距离最小。
分析:使用分治法“平衡子问题”,选择适当的分割点,分为两部分,分别求出每部分的最近点距离,另外在,分割边界出可能也存在两点距离最近。
static int[] x;
static int min = 0x7fffffff;
public static void main(String[] args) {
x = new int[]{1, 3, 5, 6, 7, 8, 10, 11};
minDis(0, x.length-1);
System.out.println(min);
}
public static void minDis(int l, int r) {
if (l+1 == r) {
min = Math.min(min, x[r]-x[l]);
return;
}
int mid = (l + r) >>> 1;
min = Math.min(min, x[mid+1]-x[mid]);
minDis(l, mid);
minDis(mid+1, r);
}
二维问题:
class Node implements Comparable<Node> {
int x;
int y;
public Node(int xx, int yy) {
x = xx;
y = yy;
}
@Override
public int compareTo(Node o) {
if (this.x < o.x)
return -1;
else if (this.x > o.x)
return 1;
else{
if (this.y < o.y)
return -1;
else if (this.y > o.y)
return 1;
else
return 0;
}
}
}
static double dmin = 0x7fffffff;
static Node[] xx;
// 二维,递归函数
public static void minDisTwoDimension(int l, int r) {
// 递归结束条件
if (l <= r)
return;
// 子问题最小规模,找出两点距离,更新最小值
if (l + 1 == r)
dmin = Math.min(dmin, getDis(l, r));
int mid = (l + r) >>> 1;
// 递归求解子问题,缩小范围
minDisTwoDimension(l, mid);
minDisTwoDimension(mid+1, r);
// 另外在mid左右两边分别可能存在一个点,该两点之间距离小于当前dmin
// 因此因为mid两边之间距离小于dmin,因此 mid-dmin ~ mid + dmin
// 所以要找到该两端点的下标
int left = binary1(l, r, (long) (xx[mid].x - dmin)), right = binary2(l, r, (long) (xx[mid].x - dmin));
// 从边界出找最近点距离
allpd(left, right);
}
// 遍历中间值两侧的所有点
static void allpd(int l, int r) {
for (int i = l<=0?1:l; i <= r; i++)
for (int j = i + 1; j <= r; j++) {
dmin = Math.min(dmin, getDis(i, j));
}
}
// 找到左边距离中间值的下标
public static int binary1(int l, int r, long d) {
while (l < r) {
int mid = (l + r) >>> 1;
if (xx[mid].x >= d)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
// 找到右边距离中间值的下标
public static int binary2(int l, int r, long d) {
while (l < r) {
int mid = (l + r) >>> 1;
if (xx[mid].x <= d)
l = mid + 1;
else
l = mid - 1;
}
return l;
}
// 两点间距离
public static double getDis(int l, int r) {
return Math.sqrt((xx[l].x - xx[r].x) * (xx[l].x - xx[r].x) + (xx[l].y - xx[r].y) * (xx[l].y - xx[r].y));
}
递归
-
找重复
1.找到一种划分方法
2.找到递归公式或者等价转化
都是父问题化为求解子问题 -
找变化的量
变化的量要作为参数 -
找出口
根据参数变化的趋势,对边界进行控制
分治法
分治法(divide and conquer),将原问题划分成若干规模较小而结构与原问题一致的子问题,递归的解决这些子问题,然后再合并其结果就得到原问题的解。
确定运行时间是分治法的优点之一
分治法在每一层递归的步骤:
- 分解(divide):将原问题分解成一系列子问题
- 解决(conquer):递归的解决各问题。若子问题足够小,则直接有解
- 合并(Combine):将子问题的结果合并成原问题的解
主定理:(master theorem)提供了用渐近符号表示许多由分治法得到的递推关系式的方法。
假设有递推关系式 ,其中n为问题规模,a为递推的子问题数量, n/b为每个子问题的规模(假设每个子问题的规模基本一样),f(n) 为递推以外进行的计算工作。
(1):f(n):O(n ^ log b ^ a - e),那么T(n)= O(n^log b ^ a)
(2):T(n) = O(n log b ^ a)
动态规划
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
概念
动态规划(英语: Dynamic programming,简称 DP) 是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划算法的核心就是记住已经解决过的子问题的解;而记住求解的方式有两种:
自顶向下的备忘录法
比如:斐波拉契数列 Fibonacci。
public static int fibonacci(int n) {
if (n <= 1)
return 1;
if (n == 2)
return 2;
return fibonacci(n-1) + fibonacci(n-2);
}
我们分析以前写过的递归就会发现有很多节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。
public class Fibonacci {
public static void main(String[] args) {
//创建备忘录
int[] memo = new int[n+1];
System.out.println(fibonacci(7));
}
/**
* 自顶向下备忘录法
* @param n
* @param memo 备忘录
* @return
*/
public static int fibonacci(int n, int[] memo) {
// 如果已经求出了fibonacci(n)的值直接返回
if(memo[n] != 0) return memo[n];
// 否则将求出的值保存在 memo 备忘录中。
if(n<=2)
memo[n]=1;
else {
memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo);
}
return memo[n];
}
}
这个方法是由上至下,比如求f(5),我们要求f(4)和f(3),求出来后放入备忘录,当求f(4)时需要f(3)和f(2),我们可以直接从备忘录取f(3)而不是再去求一遍。
自底向上的动态规划
备忘录法是利用了递归,上面算法不管怎样,计算 fib(6)的时候最后还是要计算出 fib(1), fib(2), fib(3) ……,那么何不先计算出 fib(1), fib(2), fib(3) ……,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。
public class FibonacciPlus {
/**
* 自底向上的动态规划
* @param n
* @return
*/
public static int fib(int n) {
if(n<=0)return -1;
//创建备忘录
int[] memo = new int[n+1];
memo[0]=0;
memo[1]=1;
for(int i=2;i<=n;i++) {
memo[i]=memo[i-1]+memo[i-2];
}
return memo[n];
}
/**
* 参与循环的只有 i, i-1 , i-2 三项,可以优化空间
* @param n
* @return
*/
public static int fibPlus(int n) {
if(n<=0)return -1;
int memo_i_2=0;
int memo_i_1=1;
int memo_i=1;
for(int i=2;i<=n;i++) {
memo_i = memo_i_1+memo_i_2;
memo_i_2 = memo_i_1;
memo_i_1 = memo_i;
}
return memo_i;
}
}
例题
区域和检索-数组不可变
问题描述:
给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。
解法:
- 暴力循环从下标i到j求和,如果检索次数较多,则会超出时间限制。
- 降低时间复杂度,最理想情况O(1),求前缀和,sumRange(i, j) = (0-j+1)-(0-i-1的和)
代码:
Java版
class NumArray {
int[] sums;
public NumArray(int[] nums) {
int n = nums.length;
sums = new int[n + 1];
for (int i = 0; i < n; i++) {
sums[i + 1] = sums[i] + nums[i];
}
}
public int sumRange(int i, int j) {
return sums[j + 1] - sums[i];
}
}
Python版
class NumArray:
def __init__(self, nums: List[int]):
self.sums = [0]
_sums = self.sums
for num in nums:
_sums.append(_sums[-1] + num)
def sumRange(self, i: int, j: int) -> int:
_sums = self.sums
return _sums[j+1] - _sums[i]
复杂度分析:
- 时间复杂度:
初始化需要O(n),每次检索、O(1),其中n是nums的长度。
初始化需要检索遍历数组nums的前缀和,时间复杂度O(n)。
- 空间复杂度:
空间复杂度:O(n)O(n),其中 nn 是数组 \textit{nums}nums 的长度。需要创建一个长度为 n+1n+1 的前缀和数组