通常的回溯问题,代码结构如下:
重点有三组四个结构:
void dfs(int 层深)
{
if (到达最深)//更新结构
更新答案;
标记来过;//vis结构
for(所有扩展可能)
{
if (没有扩展过)//check结构
dfs(层深+1);
}
回溯;//recursion结构,不妨和vis合称vis-rec结构
}
当然,其中的位置都根据这道题做过调整,关于标记来过
和没有扩展过
两个语句,是否放在外面取决于是否需要对进入时的第一个情形进行操作和判断。这也是回溯法不同结构的核心影响因素。
减少自己思维中算法程式的不确定性,有利于减少算法架构过程当中的意志力消耗。从而使得表现更佳的可能性增大。这里确定了vis-rec,check结构位置的意义,就可以起到这个作用。
关于上述这个回溯结构的架构原则,落实到这个问题当中,我们有如下分析:
由于需要将所有地窖都分别做一次起点,所以不妨虚设一个地窖0,(从而可以在递归函数中完成所有可能点分别为起点的搜索),这个位置是默认成立、四通八达1的,由于是虚设,其性质任意,其实第一次判断在内或者在外都不影响,因此可以将它们都放在for
的if
语句当中。
但这里为什么要把回溯部分放在外边呢……一会就会看到了,这正是法二所需的特点。
如果想要维持刚刚提出的那个原有框架即先判断是否更新,后扩展:见这条博客
利用先搜索遍历的方式看看是否这次需要扩展,即其中的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结构
}
如果以后发现这个蠢的话,一定要来更新!最起码现在看起来很棒
#include
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;
}
同样我们看到这个递归函数当中的回溯量cnt
和val
,可以考虑把它们加入递归参数当中去:
#include
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);
}
⚠敬告:没有八达…都是单向边,不要误会了!这是一个通往debug的快车 ↩︎