本专栏主要为了巩固基础算法,会把比赛中觉得比较好的题目拿过来整理一下。
本周专题dfs。
对于搜索算法,个人通过这几天的做题感悟:在写代码之前一定要想清楚要搜什么,是搜不同的排列还是搜前面所做的答案等等 ,dfs利用的是递归,它的代码很简洁,递归的决策树一定要画清楚,回溯不但可以通过在递归主体里面回,也可以在参数里面回溯,有些时候对变量的决策是突变性的,导致回溯的时候不好回,这时候可以考虑换种枚举策略。
搜素是对暴力枚举的的代码优化;
当枚举能画成一棵树时用搜索;
搜索的本质是对决策树进行遍历,直到所有的情况都收集完;
其中最重要的概念是回溯加剪枝;
一般搜索的题目数据范围都很小,且答案顺序可能要求字典序排序;
子集问题2^n - 1 组合问题cnm 排列问题n!/ n - m()! 全排列问题n!
一、B3624 猫粮规划 - 洛谷
题目来源:洛谷
题目难度:★
【解题】:本题数据范围要是小一点其实可以用二进制枚举做,但是它的时间复杂度时指数级别的会超时。每种食物都有选和不选两种选择,我们在搜索的时候分别递归两次处理即可,但是时间复杂度还是指数级别的,因此需要剪枝优化:当sum超过r时直接返回。
🖥️code:
#include <iostream>
using namespace std;
const int N = 45;
int n, l, r;
int a[N];
int cnt;
void dfs(int pos, int sum)
{
if(pos > n || sum > r)
{
if(sum <= r && sum >= l) cnt++;
return;
}
// 不选
dfs(pos + 1, sum);
// 选
dfs(pos + 1, sum + a[pos]);
}
int main(){
cin >> n >> l >> r;
for(int i = 1; i <= n; i++) cin >> a[i];
dfs(1, 0);
cout << cnt << endl;
return 0;
}
二、P1123 取数游戏 - 洛谷
题目来源:洛谷
题目难度:★
【解题】:假设这是一个简单方格选数问题,每个位置只有选与不选两种,一共会有2的次方中情况,针对于这种题二进制枚举可以解决但是时间复杂度会是687万。搜索是另一种枚举,当一个状态确定后选择下一个状态,我们将两种状态看做01,按照字典序搜索决策树,最左侧就是000...(最小的字典序),在后续深搜的过程中字典序会一次变大。之前0 1 是选出来的,在这里0所代表的是不选,我们可以通过含义操纵代码:比如函数参数是pos(所选位置),可以直接调用dfs(pos + 1) 来进行不选的操作。
回到这一题,这题多了一个限制,即不可以选择八方向相邻的数,对于这种限制我们很难从代码直接操控这种突变后选择位置的走向。因此才不去管理这种走向,让它从左到右,从上到下一次遍历,只不过我们会对遍历的位置进行标记,提醒代码这个位置是否可选。表现在代码上就是对于每次选择我们让这九个数的选择次数加一(不要标记成true,因为这个位置的状态不是只由一个选择引起的)。
🖥️code:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10;
int T, n, m;
int grid[N][N];
int visit[N][N];
int ans, sum;
int dx[] = {0, 1, 1, 1, 0, -1, -1, -1};
int dy[] = {1, 1, 0, -1, -1, -1, 0, 1};
void dfs(int cur_x, int cur_y)
{
if(cur_y > m)
{
cur_y = 1; cur_x++;
}
if(cur_x > n)
{
ans = max(ans, sum);
return;
}
// buxuan
dfs(cur_x, cur_y + 1);
// 选
if(visit[cur_x][cur_y] == 0)
{
visit[cur_x][cur_y]++;
sum += grid[cur_x][cur_y];
for(int k = 0; k < 8; k++)
{
int next_x = cur_x + dx[k];
int next_y = cur_y + dy[k];
if(next_x < 1 || next_y < 1 || next_x > n || next_y > m) continue;
visit[next_x][next_y]++;
}
dfs(cur_x, cur_y + 1);
visit[cur_x][cur_y]--;
sum -= grid[cur_x][cur_y];
for(int k = 0; k < 8; k++)
{
int next_x = cur_x + dx[k];
int next_y = cur_y + dy[k];
if(next_x < 1 || next_y < 1 || next_x > n || next_y > m) continue;
visit[next_x][next_y]--;
}
}
}
int main()
{
cin >> T;
while(T--)
{
sum = ans = 0;
memset(visit, 0, sizeof visit);
cin >> n >> m;
for(int i =1; i <= n; i++)
{
for(int j =1; j <= m; j++)
{
cin >> grid[i][j];
}
}
dfs(1, 1);
cout << ans << endl;
}
return 0;
}
三、P3848 [TJOI2007] 跳棋 - 洛谷
题目来源:洛谷
题目难度:★
【解题】:通过这道个上道可以感受到,对于搜索中下一个位置变化控制是搜索的难点。
上题是相邻的位置不能选,我们通过数组标记来跳过不合法选择,而实际上dfs还是会进入相应位置。
本题我们只能走 ‘0’ 所在的格子,对于路径上的‘1’并没有限制(意思是可以走多次),不可以走回头路,问最长的距离,同样的对于 ‘0’ 是否走过,可以用数组标记,问题是如何确定下一个位置:枚举步数利用方向向量移动。
🖥️code:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
int a[N][N];
bool st[N][N];
int n, x, y;
int ans, sum;
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
void dfs(int cur_x, int cur_y)
{
ans = max(sum, ans);
for(int k = 0; k < 4; k++)
{
int cnt = 0;
// 枚举步数
for(int step = 1; ; step++)
{
int next_x = cur_x + step * dx[k];
int next_y = cur_y + step * dy[k];
if(next_x < 1 || next_x > n || next_y < 1 || next_y > n) break; // 不合法位置
if(a[next_x][next_y] == 1) cnt++;
else
{
if(cnt >= 1 && !st[next_x][next_y]) // 这边也是剔除不合法位置
{
st[next_x][next_y] = true;
sum += cnt + 1;
dfs(next_x, next_y);
sum -= cnt + 1;
st[next_x][next_y] = false;
}
break; // 这个不能少,不管这个0能不能搜都要break
}
}
}
}
int main()
{
cin >> n >> x >> y;
for(int i =1; i <= n; i++)
{
for(int j =1; j <= n; j++)
{
cin >> a[i][j];
}
}
st[x][y] = true;
dfs(x, y);
cout << ans << endl;
return 0;
}
四、P2420 让我们异或吧 - 洛谷
题目来源:洛谷
题目难度:★
【解题】:构造从根结点异或到此节点的前缀数组,直接输出两点数组的异或结果即可。
🖥️code:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
vector<PII> edges[N];
int xor_from_root[N];
int n, Q;
bool st[N];
void dfs(int root)
{
st[root] = true;
for(auto x : edges[root])
{
int a = x.first, b = x.second;
if(!st[a])
{
xor_from_root[a] = (xor_from_root[root] ^ b); // 前序遍历
dfs(a);
}
}
}
int main()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int u, v, w; cin >> u >> v >> w;
edges[u].push_back({v, w});
edges[v].push_back({u, w});
}
dfs(1);
cin >> Q;
while(Q--)
{
int u, v; cin >> u >> v;
cout << (xor_from_root[u] ^ xor_from_root[v]) << endl;
}
return 0;
}
五、最优性剪枝
【解题】:枚举所有的排列会超时,需要换种思路:搜索车的数量,对于前面的车,可以装的下就装,不行就新开一辆。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
LL a[N];
int ans = N;
LL truck[N];
vector<int> path;
LL n, w;
bool st[N];
bool cmp(int x, int y)
{
return x > y;
}
void dfs(int pos, int cnt)
{
if(cnt >= ans) return;
if(pos > n)
{
ans = min(ans, cnt);
return;
}
for(int i = 1; i <= cnt; i++)
{
// 这里的逻辑是我从头开始选择车,一旦不能放就开一辆,有点符合贪心策略。
// 但是它也能过原因是能放一定会包含最优解(范围更广)
if(truck[i] + a[pos] > w)
{
truck[cnt + 1] += a[pos];
dfs(pos + 1, cnt + 1);
truck[cnt + 1] -= a[pos];
}
else
{
truck[i] += a[pos];
dfs(pos + 1, cnt);
truck[i] -= a[pos];
}
}
// 这里才是符合贪心的代码,当所有的车都不能放的时候我再新开一辆。
// truck[cnt + 1] += a[pos];
// dfs(pos + 1, cnt + 1);
// truck[cnt + 1] -= a[pos];
}
int main()
{
cin >> n >> w;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n, cmp); // 最优剪枝
dfs(1, 1);
cout << ans * 100 << endl;
return 0;
}