洛谷P2196 挖地雷 —— 更新易位的回溯,或dfs函数结构安排

洛谷P2196 挖地雷

回溯的一般结构与特殊结构

通常的回溯问题,代码结构如下:
重点有三组四个结构:

  • 更新结构
  • check结构
  • vis-rec结构
void dfs(int 层深)
{
	if (到达最深)//更新结构
		更新答案;
	标记来过;//vis结构
	for(所有扩展可能)
	{
		if (没有扩展过)//check结构
			dfs(层深+1);
	}
	回溯;//recursion结构,不妨和vis合称vis-rec结构
}

当然,其中的位置都根据这道题做过调整,关于标记来过没有扩展过两个语句,是否放在外面取决于是否需要对进入时的第一个情形进行操作和判断。这也是回溯法不同结构的核心影响因素。

减少自己思维中算法程式的不确定性,有利于减少算法架构过程当中的意志力消耗。从而使得表现更佳的可能性增大。这里确定了vis-rec,check结构位置的意义,就可以起到这个作用。

关于上述这个回溯结构的架构原则,落实到这个问题当中,我们有如下分析:
由于需要将所有地窖都分别做一次起点,所以不妨虚设一个地窖0,(从而可以在递归函数中完成所有可能点分别为起点的搜索),这个位置是默认成立、四通八达1的,由于是虚设,其性质任意,其实第一次判断在内或者在外都不影响,因此可以将它们都放在forif语句当中。

但这里为什么要把回溯部分放在外边呢……一会就会看到了,这正是法二所需的特点。

法一:朴素结构

如果想要维持刚刚提出的那个原有框架即先判断是否更新,后扩展:见这条博客
利用先搜索遍历的方式看看是否这次需要扩展,即其中的chck函数,这样需要搜索两次(虽然数据量很小并不太影响体验)。

void dfs(int x,int stp,int sum)//x记录现在位置,stp记录走了几个点,sum记录挖的地雷数
{
	if(chck(x))
	{
		if(maxx<sum)//更新最大值和路径
		{
			maxx=sum;
			cnt=stp;
			for(int i=1;i<=stp;i++)
			ans[i]=path[i];	
		}
		return ;
	}
	for(int i=1;i<=n;i++)//寻找下一个能去的地方
	{
		if(f[x][i]&&!b[i])
		{
			b[i]=1;//标记走过
			path[stp+1]=i;//记录路径
			dfs(i,stp+1,sum+a[i]);
			b[i]=0;//回溯
		}
		
	}
}

原博客当中没有利用虚设的地窖,而是是利用循环,分别对各个起点进行了操作。这里只需要改变调用方式,即可,代码如下:
其中虚设的地窖可以到达各个地窖。

    for(int i=1;i<=n;i++)
        f[0][i]=1;//使得虚设的地窖可以到达各个地窖
    dfs(0,0,0);

法二:更新易位的回溯结构

不鼓励这样做。算法的本质其实就是程式化解决问题。已经有了相对好的解决问题的程式,最好不要尝试自我创新。很容易出错,毕竟时间对于算法竞赛来说……不仅意味着解题的耗时,还有可能导致无谓的penalties

但既然想到了,就还是记下来。同时,它有利于我们理解vis–recursion结构位置的影响效果。

首先描述一下这种易位

标记来过;
for (所有扩展可能)
{
	if (没有去过)
		扩展;
}
if (没有扩展)
{
	更新;
}
回溯;

代码如下:

void dfs(int k)
{
    vis[k] = 1, tmp[cnt++] = k, val += num[k];
    bool flag = 0;
    for (int i = k+1; i <= n; i++)
        if (link[k][i] && !vis[i])
            dfs(i), flag = true;
    if (!flag)
    {
        if (ans < val) 
        {
            for (int i = 1; i <= cnt; i++) 
                res[i] = tmp[i];
            ans = val, cc = cnt-1;
        }
    }
    vis[k] = 0, cnt--, val -= num[k];
}

从这个代码具体观察,我们发现,vis-rec结构的作用有两条:

  • 辅助剪枝(当然在内层更可以减少调用带来的开销)
  • 作更新前的最后一次记录(当然可以在调用的时候就记录,然后直接等待最终更新)。

递归函数中这几个结构往往是灵活多变的,但是如果尝试一下对它们的功能进行实在的说明,其实会发现理解了功能之后,它们的四种组合形式都得以解决问题。

关于法二的其他思考

写着写着,既然四种组合都可以解决问题,那么自己的愚蠢岂不是显露无疑了(应该把回溯结构放在里面啊!

比较令人安慰的是如下的事实:
判断是每个情形必须判断的,但是回溯可以只进行一次。
也就是说check结构在使用过程当中,应该减少因为搜索树扩展带来的调用开销。
但vis-rec结构不一样,每个节点的多个扩展都可以公用一次vis,所以应该放在扩展for循环之外。
至此我们找到了一种合理的架构回溯问题函数的一种想法:
亦即开头的:

void dfs(int 层深)
{
	if (到达最深)//更新结构
		更新答案;
	标记来过;//vis结构
	for(所有扩展可能)
	{
		if (没有扩展过)//check结构
			dfs(层深+1);
	}
	回溯;//recursion结构
}

如果以后发现这个蠢的话,一定要来更新!最起码现在看起来很棒

附录

AC代码

#include <bits/stdc++.h>
using namespace std;

int num[30], link[30][30], n, vis[30], res[30], tmp[30], ans, val, cnt, cc;

void dfs(int k)
{
    vis[k] = 1, tmp[cnt++] = k, val += num[k];
    bool flag = 0;
    for (int i = k+1; i <= n; i++)
        if (link[k][i] && !vis[i])
            dfs(i), flag = true;
    if (!flag)
    {
        if (ans < val) 
        {
            for (int i = 1; i <= cnt; i++) 
                res[i] = tmp[i];
            ans = val, cc = cnt-1;
        }
    }
    vis[k] = 0, cnt--, val -= num[k];
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> num[i];
    for (int i = 1; i <= n; i++)
        for (int j = i+1; j <= n; j++)
            cin >> link[i][j];
    for (int i = 1; i <= n; i++) link[0][i] = 1;
    dfs(0);
    for (int i = 1; i <= cc; i++)
        cout << res[i] << ' ';
    cout << endl << ans << endl;
}

递归参数优化

同样我们看到这个递归函数当中的回溯量cntval,可以考虑把它们加入递归参数当中去:

#include <bits/stdc++.h>
using namespace std;

int num[30], link[30][30], n, vis[30], res[30], tmp[30], ans, cc;

void dfs(int k, int cnt, int val)
{
    bool flag = 0;
    vis[k] = 1, tmp[cnt++] = k, val += num[k];//记录当前结果
    for (int i = k+1; i <= n; i++)
        if (link[k][i] && !vis[i])//提前判断,不仅为当前层的flag判断提供了便利(当然可以将返回值定为bool,但……我只能想到用位运算了,还是蛮麻烦),同时也是对时间复杂度的优化
            dfs(i, cnt, val), flag = true;
    if (!flag)//这是一个位置特殊的更新
    {
        if (ans < val) 
        {
            for (int i = 0; i <= cnt; i++) 
                res[i] = tmp[i];
            ans = val, cc = cnt-1;
        }
    }
    vis[k] = 0;//回溯
}

调用仍然如此即可:

int main()
{
    dfs(0, 0, 0);
}

注释


  1. ⚠敬告:没有八达…都是单向边,不要误会了!这是一个通往debug的快车🚀 ↩︎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值