零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:输入:coins = [2], amount = 3
输出:-1
示例 3:输入:coins = [1], amount = 0
输出:0
Java代码:外循环硬币类型遍历,内循环数额增大遍历
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int MAX = 0x3f3f3f3f;
int[][] dp = new int[n + 1][amount + 1];
for (int[] ints : dp) {
Arrays.fill(ints, MAX);
}
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1]) {
dp[i][j] =min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
}
}
}
return dp[n][amount] == 0x3f3f3f3f ? -1 : dp[n][amount];
}
}
优化:从最小的数额往后遍历,内循环各种类型的硬币遍历一次
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 0; i < dp.length; i++) {
for (int coin : coins) {
if (i - coin < 0) {
continue;
}
dp[i] =min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}
零钱兑换Ⅱ
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:输入:amount = 10, coins = [10]
输出:1
区别: dp[i][j] =min(dp[i][j], dp[i][j - coins[i - 1]] + 1);改 dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
class Solution {
public int change(int amount, int[] coins) {
if (coins == null) {
return 0;
}
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= amount; j++) {
if (j >= coins[i - 1]) {
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
}
else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][amount];
}
}
最后一块石一澜的重量Ⅱ(没看懂题意)
状态压缩常见操作
与位运算相关的运算符共有 6 种,&,|,^,~,>>,<< 符号,
x & y ,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。
| 符号,x | y ,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。
^ 符号,x ^ y ,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。
<< 符号,x << y 左移操作,将 x 在二进制下的每一位向左移动 y 位,最右边用 0 填充。
>> 符号,x >> y 右移操作,将 x 在二进制下的每一位向右移动 y 位,最左边用 0 填充。
~ 符号,~x ,按位取反操作,将 x 在二进制下的每一位取反,返回其十进制下的值
用位运算可以表示集合的常见操作,如下,其中 A,B 表示两个集合,c 表示某个元素。
c 插入 A :A |= (1 << c) A
删除 c :A &= ~(1 << c) A
置空 :A = 0
并集 :A | B
交集 :A & B
全集 :(1 << n) - 1
补集 :((1 << n) - 1) ^ A
子集 :(A & B) == B
判断是否是 2 的幂 :A & (A - 1) == 0
最低位的 1 变为 0 :n &= (n - 1)
最低位的 1:A & (-A),
最低位的 1 一般记为 lowbit(A),表示 A 的二进制表达式中最低位的 1 所对应的值。
最高位的 1: int p = lowbit(A) while(p != A) { A -= p; p = lowbit(A); } return p;
枚举 A 的子集: for(subset = (A - 1) & A; subset != A; subset = (subset - 1) & A) { ... }
枚举全集的子集: for(i = 0; i <= (1 << n) - 1; ++i) { ... }
安卓系统手势解锁
我们都知道安卓有个手势解锁的界面,是一个 3 x 3 的点所绘制出来的网格。用户可以设置一个 “解锁模式” ,通过连接特定序列中的点,形成一系列彼此连接的线段,每个线段的端点都是序列中两个连续的点。如果满足以下两个条件,则 k 点序列是有效的解锁模式:
解锁模式中的所有点互不相同 。
假如模式中两个连续点的线段需要经过其他点的 中心 ,那么要经过的点 必须提前出现 在序列中(已经经过),不能跨过任何还未被经过的点。
例如,点 5 或 6 没有提前出现的情况下连接点 2 和 9 是有效的,因为从点 2 到点 9 的线没有穿过点 5 或 6 的中心。
然而,点 2 没有提前出现的情况下连接点 1 和 3 是无效的,因为从圆点 1 到圆点 3 的直线穿过圆点 2 的中心。
以下是一些有效和无效解锁模式的示例:无效手势:[4,1,3,6] ,连接点 1 和点 3 时经过了未被连接过的 2 号点。
无效手势:[4,1,9,2] ,连接点 1 和点 9 时经过了未被连接过的 5 号点。
有效手势:[2,4,1,3,6] ,连接点 1 和点 3 是有效的,因为虽然它经过了点 2 ,但是点 2 在该手势中之前已经被连过了。
有效手势:[6,5,4,1,9,2] ,连接点 1 和点 9 是有效的,因为虽然它经过了按键 5 ,但是点 5 在该手势中之前已经被连过了。
给你两个整数,分别为 m 和 n ,那么请返回有多少种 不同且有效的解锁模式 ,是 至少 需要经过 m 个点,但是 不超过 n 个点的。两个解锁模式 不同 需满足:经过的点不同或者经过点的顺序不同。
示例 1:
输入:m = 1, n = 1
输出:9
示例 2:输入:m = 1, n = 2
输出:65
状态压缩法
class Solution {
bool check(int s, int i, int j) {
int xi = i / 3, yi = i % 3;
int xj = j / 3, yj = j % 3;
int m = (xi + xj) / 2 * 3 + (yi + yj) / 2;
return (xi + xj) % 2 || (yi + yj) % 2 || s & (1 << m);
}
public:
int numberOfPatterns(int m, int n) {
// f[s][i]: 经过点集为 s,且最后经过的点是 i
int f[1 << 9][9];
memset(f, 0, sizeof(f));
for (int i = 0; i < 9; i++) {
f[1 << i][i] = 1;
}
for (int s = 1; s < 1 << 9; s++) {
for (int i = 0; i < 9; i++) {
if (s >> i & 1) {//遍历到i点时更新i为最后点的情况
for (int j = 0; j < 9; j++) {//遍历更新上个点是j点情况
if (j != i && s >> j & 1) {
if (check(s, j, i)) {
f[s][i] += f[s ^ (1 << i)][j];//继承去除i点的情况
}
}
}
}
}
}
int res = 0;
for (int s = 1; s < 1 << 9; s++) {
int c = __builtin_popcount(s);
if (c >= m && c <= n) {//点数在m和n内
for (int i = 0; i < 9; i++) {
res += f[s][i];
}
}
}
return res;
}
};
DFS:替换了状态压缩中的bool check(int s, int i, int j)函数,多了skip数组来确定可行路径。dfs函数中带起始点,路径深度及深度限制,点访问情况,总方案数和skip数组。
class Solution {
public:
int numberOfPatterns(int m, int n) {
int ans =0;
int skip[10][10];
bool visited[10];
memset(skip,0,sizeof(skip[0][0])*10*10);
memset(visited,false,sizeof(visited));
skip[1][3] = skip[3][1] =2;//当数字 1 跳到数字 3 时,中间经过的数字是 2
skip[4][6] = skip[6][4] =5;
skip[7][9] = skip[9][7] =8;
skip[1][7] = skip[7][1] =4;
skip[2][8] = skip[8][2] =5;
skip[3][9] = skip[9][3] =6;
skip[1][9] = skip[9][1] =skip[3][7] =skip[7][3] =5;
visited[0] =true;
int total =0;
visited[1] =true;
DFSHelper(1, visited, m, n, 1, total, skip);
visited[1] =false;
ans += 4 * total;//1 3 7 9
total =0;
visited[2] =true;
DFSHelper(2, visited, m, n, 1, total, skip);
visited[2] =false;
ans += 4 * total;//2 4 6 8
total =0;
visited[5] =true;
DFSHelper(5, visited, m, n, 1, total, skip);
ans += total;//5
visited[5] =false;
return ans;
}
void DFSHelper(int start, bool visited[10], int m, int n, int depth, int &total, int skip[10][10]) {
if (depth >=m) {
total ++;
}
if (depth >=n) {
return;
}
int ret =0;
for (int i=1;i<=9;i++) {
if (visited[i] ==false && visited[skip[start][i]]) {//可以到i
visited[i] =true;
DFSHelper(i, visited, m, n, depth + 1, total, skip);
visited[i] =false;
}
}
}
};