一、LeetCode每日一题
题目链接
题目大意
给定一个字符串数组 a r r arr arr,字符串 s s s是将 a r r arr arr某一子序列字符串连接所得的字符串,如果 s s s中的每一个字符都只出现过一次,那么它就是一个可行解。
请返回所有可行解 s s s中最长长度。
题解
观察数据范围 1 < = a r r . l e n g t h < = 16 1 <= arr.length <= 16 1<=arr.length<=16,提示我们可以设计最坏 O ( 2 n ) O(2^n) O(2n)的算法。
-
错误解法(排序+贪心)
- 思路如下,首先直觉告诉我们如果单个字符串越长,就越有可能给答案贡献越多的字符。
- 那么我们只要按照字符串的长度逆序排序后,贪心的去尝试选每个字符串即可(能选则选)。
- 其实这样的决策是错误的。例如: [ " a b c d " , " a b c " , " d e f " ] ["abcd", "abc", "def"] ["abcd","abc","def"],这样的话我们的贪心决策总是会把 " a b c d " "abcd" "abcd"这个字符串选走,而后边的字符串都无法选择。导致错过 " a b c " , " d e f " "abc","def" "abc","def"这个最优解。
-
正确解法一(二进制枚举)
- 因为数据范围很小,我们可以枚举所有的选择的可能性,也就是数组长度 n n n的幂集,而幂集最多只有 2 16 = 65536 2^{16}=65536 216=65536种可能,所以完全不用担心 T L E TLE TLE
- 下边介绍啥叫二进制枚举:
- 众所周知一个每一个十进制数都有且仅有唯一的二进制数与之对应。
- 例如: 3 3 3的二进制就是 11 1 b 111_b 111b, 4 4 4的二进制是 10 0 b 100_b 100b。
- 那么我们可以刚好利用每一个二进制数的数位,因为这一位不是 0 0 0就是 1 1 1,我们用 1 1 1代表选择对应下标的字符, 0 0 0代表不选。例如: 011011 0 b 0110110_b 0110110b,这个二进制数就代表选择 1 、 2 、 4 、 5 1、2、4、5 1、2、4、5下标的字符串,而不选 0 、 3 、 6 0、3、6 0、3、6下标的字符串。
- 那么我们要枚举的范围是多少呢?
- 显然是从 0000..0 0 b 0000..00_b 0000..00b到 111...1 1 b 111...11_b 111...11b,一堆 0 0 0代表全不选,一堆 1 1 1代表全选。
- 那这个 111...1 1 b 111...11_b 111...11b对应的十进制数是多少呢?
- 对,没错是 2 n − 1 2^n - 1 2n−1,因为你的数组长度 n n n的每一位都有选与不选两种状态,如果全选就是 [ 0 , n − 1 ] [0,n-1] [0,n−1]位上每一位都是1。
- 所以枚举的范围也就是 [ 0 , 2 n ) [0,2^n) [0,2n)
- 会二进制枚举了,要怎么检查呢?我们仍然可以用一个
26
26
26位的二进制数去表示
[
a
,
z
]
[a,z]
[a,z]这
26
26
26个字母有没有被选到,最后只要统计这个二进制数中的
1
1
1的个数即可。
- 最后再发一个小技巧。其实我们可以一步操做找到当前二进制数对应的最后一个 1 1 1的值这个操做叫做 l o w b i t ( x ) = x & − x lowbit(x)=x \& -x lowbit(x)=x&−x。
- 例如: l o w b i t ( 20 ) lowbit(20) lowbit(20),就是找到 20 20 20对应二进制 1010 0 b 10100_b 10100b,的最低位 1 1 1所对应的值,也就是 10 0 b = 8 100_b=8 100b=8
public int maxLength(List<String> arr) {
int n = arr.size();
int lim = 1 << n;
int ans = 0;
// 枚举1 - (2^n -1),因为0000..0就代表啥都不选,所以不用考虑
for (int i = 1; i < lim; i++) ans = Math.max(get(arr, i), ans);
return ans;
}
private int get(List<String> arr, int state) {
// 26位的二进制数
int used = 0;
// 枚举每一位
int j = 0;
while (true) {
int curPos = 1 << j;
// 如果当前的1的位置超过了state的左边界
if (curPos > state) break;
// 如果state第j位上是1,也就代表要选对应的字符串
if ((state & curPos) > 0) {
char[] cur = arr.get(j).toCharArray();
for (char c : cur) {
int ope = 1 << (c - 'a');
// 如果这个字母被占用了,就返回这是种错误的情况
if ((used & ope) > 0) return -1;
used |= ope;
}
}
j++;
}
// popCount();
int ret = 0;
// 看看当前数字能消去几次1,也就代表这个数字由多少个1构成
while (used > 0) {
ret++;
used -= (used & -used);
}
return ret;
}
- 递归版本
int ans = 0, n;
public int maxLength(List<String> arr) {
n = arr.size();
dfs(arr, 0, 0);
return ans;
}
private void dfs(List<String> arr, int cur, int alpha) {
if (cur == n) {
ans = Math.max(ans, popCount(alpha));
return;
}
// 不选当前的
dfs(arr, cur + 1, alpha);
// 选当前的字符串
char[] s = arr.get(cur).toCharArray();
for (char c : s) {
int bit = 1 << (c - 'a');
// 这一个字母被占用了,直接返回
if ((alpha & bit) != 0) return;
// 该位置为1
alpha |= bit;
}
dfs(arr, cur + 1, alpha);
}
private int popCount(int v) {
int ret = 0;
while (v > 0) {
v -= (v & -v);
ret++;
}
return ret;
}
时空复杂度分析
- 非递归版本
- 时间复杂度 O ( 2 n ∗ ∣ Σ S ∣ ) O(2^n*|\Sigma S|) O(2n∗∣ΣS∣)
- 只使用了常数个变量 O ( 1 ) O(1) O(1)
- 递归版本
- 时间复杂度 O ( 2 n ∗ ∣ Σ S ∣ ) O(2^n*|\Sigma S|) O(2n∗∣ΣS∣) ,但是因为我们及时剪枝 ,所以复杂度应该更小,且实际测试更快。
- 递归的调用栈占用 O ( 2 n ) O(2^n) O(2n)
二、CodeForces题解
1. Arithmetic Array
题目
题目大意
给你 n n n个数字,输出至少还需添加几个 ≥ 0 \geq 0 ≥0的数使得这些数的算数平均数为 1 1 1。
- 算数平均数: a 1 + a 2 + a 3 + . . . + a i i \frac{a_1 + a_2 + a_3 +...+a_i}{i} ia1+a2+a3+...+ai
- 例子:
[
1
,
2
]
[1,2]
[1,2],至少还要添加一个0,使得
1
+
2
+
0
3
=
1
\frac{1+2+0}{3}=1
31+2+0=1
题解
观察数据范围 1 < = n < = 50 1<=n<=50 1<=n<=50, − 1 0 4 ≤ a i ≤ 1 0 4 -10^4\leq a_i \leq 10^4 −104≤ai≤104 提示我们可以设计一个最坏是 O ( n 4 ) O(n^4) O(n4)的算法。
- 感觉是分类讨论的问题,所以就讨论即可。
- a 1 + a 2 + a 3 + . . . + a i a_1 + a_2 + a_3 +...+a_i a1+a2+a3+...+ai记作 s u m sum sum
- 如果 s u m < 0 sum < 0 sum<0,那么我们只要添加一个 − s u m + n + 1 -sum + n + 1 −sum+n+1即可,所以答案是 1 1 1
- 如果 0 ≤ s u m < n 0\leq sum < n 0≤sum<n, 那么我们添加一个 n − s u m + 1 n - sum + 1 n−sum+1即可,所以答案是 1 1 1
- 如果 s u m = = n sum == n sum==n,答案是 0 0 0
- 如果 s u m > n sum>n sum>n,那么我们可以添加一堆 0 0 0直到 s u m = = n sum==n sum==n,所以答案是 s u m − n sum-n sum−n。
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n;
cin >> n;
int sum = 0;
for (int i = 0, v; i < n; ++i) {
cin >> v;
sum += v;
}
if (sum < 0) {
cout << 1 << endl;
} else if (sum == n) {
cout << 0 << endl;
} else cout << max(sum - n, 1) << endl;
}
int main() {
int t;
cin >> t;
while (t--) solve();
return 0;
}
时空复杂度分析
- 读入数据是 O ( n ) O(n) O(n)的,而输出结果为 O ( 1 ) O(1) O(1),所以总的时间复杂度为 O ( n ) O(n) O(n)
- 只用了常数个变量,总空间复杂度 O ( 1 ) O(1) O(1)
2. Bad Boy
题目
题目大意
给你 n 、 m n、m n、m代表 n ∗ m n*m n∗m的网格,再给你一个起点 s = ( i , j ) s=(i,j) s=(i,j),请你选出两个点,使得从起点 s s s出发经过这两点并回到 s s s的路径最长。(你总会走最优的路径)。
题解
观察数据范围 1 ≤ n , m ≤ 1 0 9 , 1 ≤ i ≤ n , 1 ≤ j ≤ m 1≤n,m≤10^9, 1≤i≤n, 1≤j≤m 1≤n,m≤109,1≤i≤n,1≤j≤m 提示我们可以设计一个最坏是 O ( log n ) O(\log n) O(logn)的算法。
- 首先,日常生活中的经验告诉我们,这两点肯定得放的尽可能的远,那么这两点应该是在对角线上。
- 其次,这两点形成的连线应该同起点尽可能远。
- 所以分类即可:
a
,
b
a,b
a,b代表选择的两个点,红色方框代表
s
s
s所在的位置.
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n, m, i, j;
cin >> n >> m >> i >> j;
if (i <= n / 2) {
if (j <= m / 2) printf("1 %d %d 1\n", m, n);
else printf("1 1 %d %d\n", n, m);
} else {
if (j <= m / 2) printf("1 1 %d %d\n", n, m);
else printf("%d 1 1 %d\n", n, m);
}
}
int main() {
int t;
cin >> t;
while (t--) solve();
return 0;
}
时空复杂度分析
- 输出结果为 O ( 1 ) O(1) O(1),所以总的时间复杂度为 O ( 1 ) O(1) O(1)
- 只用了常数个变量,总空间复杂度 O ( 1 ) O(1) O(1)