每日一题做题记录,参考官方和三叶的题解 |
题目要求
思路一:BFS
- 找从 s t a r t start start状态到 e n d end end状态的最小步数;
- 用哈希表存每个状态和步数;
- 维护一个遍历队列,取队头元素 c u r cur cur,依次替换每一位得到下一个可能状态,合法(出现在bank中)即加入队列并更新步数;
- 直到下一个可能状态与 e n d end end相等,返回 s t e p + 1 step+1 step+1。
Java
class Solution {
static char[] items = new char[]{'A', 'C', 'G', 'T'};
public int minMutation(String start, String end, String[] bank) {
Set<String> bankSet = new HashSet<>();
for(String s : bank)
bankSet.add(s);
Deque<String> que = new ArrayDeque<>();
Map<String, Integer> map = new HashMap<>();
que.addLast(start);
map.put(start, 0); // 步数为0
while(!que.isEmpty()) {
String cur = que.pollFirst();
char[] cs = cur.toCharArray();
int step = map.get(cur);
for(int i = 0; i < 8; i++) {
for(char c : items) {
if(cs[i] == c)
continue;
char[] clone = cs.clone();
clone[i] = c; // 换第i位为c
String nxt = String.valueOf(clone);
if(!bankSet.contains(nxt)) // 是否合法出现在bank里
continue;
if(map.containsKey(nxt)) // 是否记录过
continue;
if(nxt.equals(end)) // 是否到结尾
return step + 1;
map.put(nxt, step + 1); // 更新步数
que.addLast(nxt); // 压入队列
}
}
}
return -1;
}
}
- 时间复杂度: O ( C × n ) O(C\times n) O(C×n),其中 C = 32 = 8 × 4 C=32=8\times4 C=32=8×4,即四个基因分别替换八个位置
- 空间复杂度: O ( n ) O(n) O(n)
C++
static char items[4] = {'A', 'C', 'G', 'T'};
class Solution {
public:
int minMutation(string start, string end, vector<string>& bank) {
queue<string> que;
unordered_map<string, int> map;
que.push(start);
map[start] = 0; // 步数为0
while(!que.empty()) {
string cur = que.front();
que.pop();
int step = map[cur];
for(int i = 0; i < 8; i++) {
for(char c :items) {
if(cur[i] == c)
continue;
string nxt = cur;
nxt[i] = c; // 换第i位为c
if(find(bank.begin(), bank.end(), nxt) == bank.end()) // 是否合法出现在bank里
continue;
if(map.count(nxt)) // 是否记录过
continue;
if(nxt == end) // 是否到结尾
return step + 1;
map[nxt] = step + 1; // 更新步数
que.push(nxt);
}
}
}
return -1;
}
};
- 时间复杂度: O ( C × n ) O(C\times n) O(C×n),其中 C = 32 = 8 × 4 C=32=8\times4 C=32=8×4,即四个基因分别替换八个位置
- 空间复杂度: O ( n ) O(n) O(n)
思路二:双向BFS
- BFS可能出现搜索空间爆炸问题,即当前层宽度超级大;
- 从头尾分别开始搜索,直至当前遍历值出现在对面。
Java
class Solution {
String start, end;
Set<String> bankSet = new HashSet<>();
static char[] items = new char[]{'A', 'C', 'G', 'T'};
public int minMutation(String start, String end, String[] bank) {
this.start = start;
this.end = end;
for(String s : bank)
bankSet.add(s);
return bfs();
}
int bfs() {
Deque<String> que1 = new ArrayDeque<>(), que2 = new ArrayDeque<>();
Map<String, Integer> map1 = new HashMap<>(), map2 = new HashMap<>();
que1.addLast(start);
map1.put(start, 0);
que2.addLast(end);
map2.put(end, 0);
while(!que1.isEmpty() && !que2.isEmpty()) {
int step = -1;
if(que1.size() <= que2.size()) // 平均二者空间
step = update(que1, map1, map2);
else
step = update(que2, map2, map1);
if(step != -1)
return step;
}
return -1;
}
int update(Deque<String> que, Map<String, Integer> thisMap, Map<String, Integer> thatMap) {
int size = que.size();
while(size-- > 0) {
String cur = que.pollFirst();
char[] cs = cur.toCharArray();
int step = thisMap.get(cur);
for(int i = 0; i < 8; i++) {
for(char c : items) {
if(cs[i] == c)
continue;
char[] clone = cs.clone();
clone[i] = c; // 换第i为为c
String nxt = String.valueOf(clone);
if(!bankSet.contains(nxt)) // 是否合法出现在bank里
continue;
if(thatMap.containsKey(nxt)) // 是否在另一边遍历过
return step + 1 + thatMap.get(nxt);
if(!thisMap.containsKey(nxt) || thisMap.get(nxt) > step + 1) { // 更新队列
thisMap.put(nxt, step + 1);
que.addLast(nxt);
}
}
}
}
return -1;
}
}
- 时间复杂度: O ( C × n ) O(C\times n) O(C×n),其中 C = 32 = 8 × 4 C=32=8\times4 C=32=8×4,即四个基因分别替换八个位置
- 空间复杂度: O ( n ) O(n) O(n)
C++
【不道为啥超时间限制了……调了半天……就糊弄了】
static char items[4] = {'A', 'C', 'G', 'T'};
class Solution {
string start, end;
vector<string> bank;
public:
int minMutation(string start, string end, vector<string>& bank) {
this->start = start;
this->end = end;
this->bank = bank;
return bfs();
}
int bfs() {
queue<string> que1, que2;
unordered_map<string, int> map1, map2;
que1.push(start);
map1[start] = 0;
que2.push(end);
map2[end] = 0;
while(!que1.empty() && !que2.empty()) { // 平均二者空间
int step = -1;
if(que1.size() <= que2.size())
step = update(que1, map1, map2);
else
step = update(que2, map2, map1);
if(step != -1)
return step;
}
return -1;
}
int update(queue<string> que, unordered_map<string, int> thisMap, unordered_map<string, int> thatMap) {
int size = que.size();
while(size-- > 0) {
string cur = que.front();
que.pop();
int step = thisMap[cur];
for(int i = 0; i < 8; i++) {
for(char c : items) {
if(cur[i] == c)
continue;
string nxt = cur;
nxt[i] = c; // 换第i为为c
if(find(bank.begin(), bank.end(), nxt) == bank.end()) // 是否合法出现在bank里
continue;
if(thatMap.count(nxt)) // 是否在另一边遍历过
return step + 1 + thatMap[nxt];
if(!thisMap.count(nxt) || thisMap[nxt] > step + 1) { // 更新队列
thisMap[nxt] = step + 1;
que.push(nxt);
}
}
}
}
return -1;
}
};
- 时间复杂度: O ( C × n ) O(C\times n) O(C×n),其中 C = 32 = 8 × 4 C=32=8\times4 C=32=8×4,即四个基因分别替换八个位置
- 空间复杂度: O ( n ) O(n) O(n)
思路三:A*
- 计算最小转换步数作为启发式函数,进行启发式搜索;
- 使用优先队列维护状态, 启 发 值 = 最 小 转 换 步 数 启发值=最小转换步数 启发值=最小转换步数的状态优先出队扩展。
【启发式搜索分析时空复杂度意义不大】
A star算法
- 学习参考链接
- 一种启发式搜索算法,可以看作是升级版Dijkstra;
- 找最小的
F
=
G
+
H
F=G+H
F=G+H
- G G G是起点到当前节点的距离;
- H H H是当前节点到终点的估计距离(启发式),通常使用曼哈顿距离估算。
Java
class Solution {
class Node {
String s;
int val; // 到最终状态的距离
Node(String _s) {
s = _s;
for(int i = 0; i < 8; i++)
if(s.charAt(i) != end.charAt(i))
val++;
}
}
static char[] items = new char[]{'A', 'C', 'G', 'T'};
String start, end;
public int minMutation(String start, String end, String[] bank) {
Set<String> bankSet = new HashSet<>();
for (String s : bank)
bankSet.add(s);
this.start = start;
this.end = end;
PriorityQueue<Node> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
Map<String, Integer> map = new HashMap<>();
pq.add(new Node(start));
map.put(start, 0); // 步数为0
while(!pq.isEmpty()) {
Node cur = pq.poll();
char[] cs = cur.s.toCharArray();
int step = map.get(cur.s);
for(int i = 0; i < 8; i++) {
for(char c : items) {
if(cs[i] == c)
continue;
char[] clone = cs.clone();
clone[i] = c; // 换第i位为c
String nxt = String.valueOf(clone);
if(!bankSet.contains(nxt)) // 是否合法出现在bank里
continue;
if(nxt.equals(end)) // 是否到结尾
return step + 1;
if(!map.containsKey(nxt) || map.get(nxt) > step + 1) { // 更新队列
map.put(nxt, step + 1);
pq.add(new Node(nxt));
}
}
}
}
return -1;
}
}
C++
【自定义类很难哦,还有priority_queue
好久不用都忘了,调了半天】
static char items[4] = {'A', 'C', 'G', 'T'};
class Solution {
string start, end;
public:
class Node {
public:
string s;
int val; // 到最终状态的距离
Node(string _s) {
s = _s;
Solution solu;
for(int i = 0; i < 8; i++)
if(s[i] != solu.end[i])
val++;
}
};
int minMutation(string start, string end, vector<string>& bank) {
auto cmp = [](Node a, Node b) { return a.val > b.val; };
priority_queue<Node, vector<Node>, decltype(cmp)> pq(cmp);
unordered_map<string, int> map;
pq.push(Node(start));
map[start] = 0; // 步数为0
while(!pq.empty()) {
Node cur = pq.top();
pq.pop();
int step = map[cur.s];
for(int i = 0; i < 8; i++) {
for(char c : items) {
if(cur.s[i] == c)
continue;
string nxt = cur.s;
nxt[i] = c; // 换第i位为c
if(find(bank.begin(), bank.end(), nxt) == bank.end()) // 是否合法出现在bank里
continue;
if(nxt == end) // 是否到结尾
return step + 1;
if(!map.count(nxt) || map[nxt] > step + 1) { // 更新队列
map[nxt] = step + 1;
pq.push(Node(nxt));
}
}
}
}
return -1;
}
};
思路四:建图+DFS
- 建图,两个字符仅有一位不同则有边;
- 遍历List建图,以 s t a r t start start开始 e n d end end结束;
- 跑一遍DFS;
- 避免环or无解,设置最大搜索深度 n n n和最优解剪枝避免死循环。
Java
class Solution {
int N = 15, M = 15 * 15 * 2 + 50, idx =0, loc = 1;
int[] head = new int[N], edge = new int[M], next = new int[M];
int n, res;
void add(int a, int b) {
edge[idx] = b;
next[idx] = head[a];
head[a] = idx++;
}
void dfs(int u, int fa, int depth) {
if(depth >= res)
return ; // 最优解剪枝
if(u == n) {
res = depth;
return ;
}
for(int i = head[u]; i != -1; i = next[i]) {
int j = edge[i];
if(j == fa)
continue;
dfs(j, u, depth + 1);
}
}
public int minMutation(String start, String end, String[] bank) {
List<String> list = new ArrayList<>();
list.add(start);
boolean ok = false;
for(String s : bank) {
if(s.equals(start))
continue;
if(s.equals(end)) {
ok = true;
continue;
}
list.add(s);
}
if(!ok) // 到不了end
return -1;
list.add(end);
n = list.size();
res = n;
Arrays.fill(head, -1);
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
if(i == j)
continue;
int cnt = 0;
for(int k = 0; k < 8 && cnt <= 1; k++)
if(list.get(i).charAt(k) != list.get(j).charAt(k))
cnt++;
if(cnt == 1){ // 只有一位不同,连一条边
add(i + 1, j + 1);
add(j + 1, i + 1);
}
}
}
dfs(1, -1, 0); // 初始深度为0
return res == n ? -1 : res;
}
}
- 时间复杂度: O ( C × n 2 ) O(C\times n^2) O(C×n2),预处理出 L i s t List List的复杂度为 O ( n ) O(n) O(n);建图复杂度为 O ( C × n 2 ) O(C\times n^2) O(C×n2),其中 C = 8 C=8 C=8;DFS有最大深度约束复杂度为 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2),最坏情况完全图
C++
【会报错,不知道咋肥死会超出索引边界,也不知道哪个edge去做了撒子】
const static int N = 15, M = 15 * 15 * 2 + 50;
class Solution {
int idx = 0;
int head[N], edge[M], next[M];
int n, res;
public:
void add(int a, int b) {
edge[idx] = b;
next[idx] = head[a];
head[a] = idx++;
}
void dfs(int u, int fa, int depth) {
if(depth >= res)
return ; // 最优解剪枝
if(u == n) {
res = depth;
return ;
}
for(int i = head[u]; i != -1; i = next[i]) {
int j = edge[i]; // error: out of bounds
if(j == fa)
continue;
dfs(j, u, depth + 1);
}
}
int minMutation(string start, string end, vector<string>& bank) {
vector<string> list;
list.push_back(start);
bool ok = false;
for(auto s : bank) {
if(s == start)
continue;
if(s == end) {
ok = true;
continue;
}
list.push_back(s);
}
if(!ok)
return -1;
list.push_back(end);
n = list.size();
res = n;
memset(head, -1, N);
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
if(i == j)
continue;
int cnt = 0;
for(int k = 0; k < 8 && cnt <= 1; k++)
if(list[i][k] != list[j][k])
cnt++;
if(cnt == 1) {
add(i + 1, j + 1);
add(j + 1, i + 1);
}
}
}
dfs(1, -1, 0);
return res == n ? -1 : res;
}
};
- 时间复杂度: O ( C × n 2 ) O(C\times n^2) O(C×n2),预处理出 L i s t List List的复杂度为 O ( n ) O(n) O(n);建图复杂度为 O ( C × n 2 ) O(C\times n^2) O(C×n2),其中 C = 8 C=8 C=8;DFS有最大深度约束复杂度为 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2),最坏情况完全图
总结
今天的题解法好多,周末心思不太正,浅学了一下A star但感觉没有理解透,不过搞懂了双向BFS(安慰自己,但其实代码都没跑通)。最后DFS还复习了一下链式前向星,哦好多知识,任重道远。
留了两个报错小尾巴,C++好烦哦嘤嘤嘤,之后要来好好复习!!!
欢迎指正与讨论! |