从删库到跑路:我的算法救赎之路 Day5

本专栏主要为了巩固基础算法,会把比赛中觉得比较好的题目拿过来整理一下。

本周专题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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值