YbtOJ 「基础算法」 第4章 深度搜索
深搜
A. 【例题1】拔河比赛
[题目描述]
在浙江师范大学 ACM 集训队,队员平时集训脑力劳动力比较重。为了劳逸结合,我们敬爱的韩老师准备了一场拔河比赛,让队员放松心情。
为了拔河比赛的公正性,韩老师提出以下要求:
\((1)\)拔河比赛两边人数最多不能相差\(1\)。
\((2)\)每个队员都有体重,我们要使两边比赛的人体重和相差最小。
现在有\(N\)个队员,韩老师想你帮忙分配,并且把分配后两边体重和之差最小值输出。
[输入格式]
首先输入\(T\),表示有\(T\)组样例。
每个样例:
首先输入人数\(N\),占一行。
后面跟着\(N\)个数,表示\(N\)的体重\(W_1-W_n\)。
[输出格式]
对于每个样例输出一行,一个整数表示两边体重之差的绝对值。
[算法分析]
直接深搜 每一个点选或者不选两种状态 搜到底了更新答案
加了可行性剪枝反倒更慢(?)
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int T , n , a[N] , ans , sum;
void dfs ( int stp , int peo , int wei )
{
if ( stp == n )
{
if ( abs ( n - peo - peo ) <= 1 ) ans = min ( ans , abs ( sum - wei - wei ) );
return;
}
if ( peo > n / 2 + 2 ) return;
dfs ( stp + 1 , peo , wei );
dfs ( stp + 1 , peo + 1 , wei + a[stp] );
}
signed main ()
{
T = read();
while ( T -- )
{
n = read() , ans = sum = 0;
for ( int i = 1 ; i <= n ; i ++ ) sum += ( a[i] = read() );
ans = sum;
dfs ( 1 , 0 , 0 );
printf ( "%d\n" , ans );
}
return 0;
}
B. 【例题2】数独游戏
[题目描述]
数独是一种传统益智游戏,你需要把\(n*n\)的数独补充完整,使得图中每行、每列、每个\(3*3\)的九宫格内数字\(1-9\)均恰好出现一次。
请编写一个程序填写数独。
[输入格式]
输入包含多组测试用例。
每个测试用例占一行,包含\(9*9\)个字符,表达数独的\(9*9\)个格内数据(顺序总体由上到下,同行由左到右)。
每个字符都是一个数字或一个 .
(表示尚未填充)。
您可以假设输入中的每一个谜题都只有一个解决方案。
文件结尾处包含单词 end
的单行,表示输入结束。
[输出格式]
每个测试用例,输出一行数据,表示填充完全后的数独。
[算法分析]
考虑用位运算表示每一行/列/九宫格中1-9数字的包含情况
判断是否有重复情况:
if ( sqr[x/3][y/3] & ( 1 << i ) ) continue;
if ( line[x] & ( 1 << i ) ) continue;
if ( row[y] & ( 1 << i ) ) continue;
修改这一位上的值为1:
sqr[x/3][y/3] |= ( 1 << i );
line[x] |= ( 1 << i );
row[y] |= ( 1 << i );
将这一位上的1变为0
sqr[x/3][y/3] ^= ( 1 << i );
line[x] ^= ( 1 << i );
row[y] ^= ( 1 << i );
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int T , n , mp[9][9] , sqr[3][3] , line[9] , row[9] , flag;
void dfs ( int stp )
{
if ( stp == 81 )
{
for ( int i = 0 ; i < 9 ; i ++ )
for ( int j = 0 ; j < 9 ; j ++ )
printf ( "%d" , mp[i][j] );
printf ( "\n" ) , flag = 1;
return;
}
int x = stp / 9 , y = stp % 9;
if ( mp[x][y] ) return dfs ( stp + 1 );
for ( int i = 1 ; i <= 9 ; i ++ )
{
if ( sqr[x/3][y/3] & ( 1 << i ) ) continue;
if ( line[x] & ( 1 << i ) ) continue;
if ( row[y] & ( 1 << i ) ) continue;
sqr[x/3][y/3] |= ( 1 << i );
line[x] |= ( 1 << i );
row[y] |= ( 1 << i );
mp[x][y] = i;
dfs ( stp + 1 );
if ( flag ) return;
mp[x][y] = 0;
sqr[x/3][y/3] ^= ( 1 << i );
line[x] ^= ( 1 << i );
row[y] ^= ( 1 << i );
}
}
void init()
{
memset ( mp , 0 , sizeof(mp) );
memset ( sqr , 0 , sizeof(sqr) );
memset ( line , 0 , sizeof(line) );
memset ( row , 0 , sizeof(row) );
flag = 0;
}
signed main ()
{
string s;
while ( cin >> s && s != "end" )
{
init();
for ( int i = 0 ; i < 9 ; i ++ )
for ( int j = 0 ; j < 9 ; j ++ )
if ( s[i*9+j] != '.' )
{
mp[i][j] = s[i*9+j] - 48;
sqr[i/3][j/3] |= ( 1 << ( s[i*9+j] - 48 ) );
line[i] |= ( 1 << ( s[i*9+j] - 48 ) );
row[j] |= ( 1 << ( s[i*9+j] - 48 ) );
}
dfs(0);
}
return 0;
}
C. 【例题3】虫食算
[题目描述]
所谓虫食算,就是原先的算式中有一部分被虫子啃掉了,需要我们根据剩下的数字来判定被啃掉的字母。来看一个简单的例子:
43#9865#045
+ 8468#6633
44445509678
其中 #
号代表被虫子啃掉的数字。根据算式,我们很容易判断:第一行的两个数字分别是 \(5\) 和 \(3\),第二行的数字是 \(5\)。
现在,我们对问题做两个限制:
首先,我们只考虑加法的虫食算。这里的加法是 \(n\) 进制加法,算式中三个数都有 \(n\) 位,允许有前导的 \(0\)。
其次,虫子把所有的数都啃光了,我们只知道哪些数字是相同的,我们将相同的数字用相同的字母表示,不同的数字用不同的字母表示。如果这个算式是 \(n\) 进制的,我们就取英文字母表的前 \(n\) 个大写字母来表示这个算式中的 \(0\) 到 \(n - 1\) 这 \(n\) 个不同的数字:但是这 \(n\) 个字母并不一定顺序地代表 \(0\) 到 \(n-1\)。输入数据保证 \(n\) 个字母分别至少出现一次。
BADC
+CBDA
DCCC
上面的算式是一个4进制的算式。很显然,我们只要让 \(ABCD\) 分别代表 \(0123\),便可以让这个式子成立了。你的任务是,对于给定的 \(n\) 进制加法算式,求出 \(n\) 个不同的字母分别代表的数字,使得该加法算式成立。输入数据保证有且仅有一组解。
[输入格式]
输入的第一行是一个整数 \(n\),代表进制数。
第二到第四行,每行有一个由大写字母组成的字符串,分别代表两个加数以及和。这 \(3\) 个字符串左右两端都没有空格,从左到右依次代表从高位到低位,并且恰好有 \(n\) 位。
[输出格式]
输出一行 \(n\) 个用空格隔开的整数,分别代表 \(A,B, \dots\) 代表的数字。
[算法分析]
考虑搜索 必须从后向前搜索(因为使用这一位的时候需要使用上一位进位) 题目中出现的数字保存在一个数组中 dfs查询时使用
对于每一个字母 枚举所有没被使用过的数字即可
接下来考虑剪枝 从前往后考虑每一列三个数(先将这三个数提取出来)
-
三个数都确定
- 进位也确定:当且仅当上一列三个数都是确定的 可以计算出来进位是0/1 那么判断\((x+y+jin)\% n\)是否为\(z\) 再判断最高位不能有进位 做出剪枝
- 进位不确定:上一列三个数不全确定 那么如果$( x + y ) % n != z \(且\) ( x + y + 1 ) % n != z$ 就是不符合情况的 再判断最高位不能有进位
-
三个数不确定:
直接令\(jin=-1\)
[代码实现]
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
const int inf = 0x3f3f3f3f;
inline int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , flag , ans[N] , q[N] , cnt , vis[N];//ans是字母(字母减掉64对应的数字)作为下标,字母对应的数字作为值的一个数组 vis[i]表示这个数字用没用过
string s[4];
bool check()
{
int jin = 0;//进位
for ( int i = n - 1 ; i >= 0 ; i -- )
{
int x = ans[s[1][i]-64] , y = ans[s[2][i]-64] , z = ans[s[3][i]-64];
if ( x != -1 && y != -1 && z != -1 )
{
if ( jin != -1 )//确定进位(0/1)
{
if ( ( x + y + jin ) % n != z ) return 0;//有确定的进位 只需要判断一种情况
if ( i == 0 && ( x + y + jin ) / n ) return 0;//最高位不应该有进位
jin = ( x + y + jin ) / n;//更新进位
}
else //不确定进位
{
if ( ( x + y ) % n != z && ( x + y + 1 ) % n != z ) return 0;//将进位和不进位两种情况讨论一下
if ( i == 0 && ( x + y ) / n ) return 0;//最高位不应该有进位
}
}else jin = -1;//整个式子不确定 无法判断进位
}
return 1;
}
void dfs ( int stp )
{
if ( stp > n ) { for ( int i = 1 ; i <= n ; i ++ ) printf ( "%d " , ans[i] ); flag = 1; return; }
for ( int i = 0 ; i < n ; i ++ )
if ( !vis[i] )
{
vis[i] = 1 , ans[q[stp]] = i;
if ( check() )//可行性剪枝
dfs ( stp + 1 );
if ( flag ) return;
vis[i] = 0 , ans[q[stp]] = -1;
}
}
signed main ()
{
n = read();
cin >> s[1] >> s[2] >> s[3];
for ( int i = n - 1 ; i >= 0 ; i -- ) //从后往前(很重要
for ( int j = 1 ; j <= 3 ; j ++ )
if ( !vis[s[j][i] - 64] )
vis[s[j][i]-64] = 1 , q[++cnt] = s[j][i] - 64;
memset ( vis , 0 , sizeof ( vis ) );
memset ( ans , -1 , sizeof ( ans ) );
dfs(1);
return 0;
}
D. 1.生日蛋糕
剪枝有三种:
- 可行性剪枝 后面的最小体积加上当前体积如果超过限制就返回
- 最优性剪枝 后面的最小表面积加上当前表面积如果更劣就返回
- 最优性剪枝 将体积和表面积综合起来 算出后面的理论最小表面积 如果更劣就返回
这里详细推一下3:
\(1\) 到 \(\operatorname{dep}-1\) 的体积为
\(n-v=\sum_{k=1}^{d e p-1} h[k] * r[k]^{2}\)
表面积为
\(2*\sum_{k=1}^{d e p-1} h[k] * r[k]\)
$\because 2*\sum_{k=1}^{d e p-1} h[k] * r[k]=\frac{2}{r[d e p]} * \sum_{k=1}^{d e p-1} h[k] * r[k] * r[d e p] \geqslant \frac{2}{r[d e p]} * \sum_{k=1}^{d e p-1} h[k] * r[k]^{2} \geqslant \frac{2(n-v)}{r[d e p]} $
\(\therefore\) 当 \(\frac{2(n-v)}{r[d e p]}+s \geqslant a n s\) 时说明已经不是最优,即可return
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid ((l+r)>>1)
#define int long long
const int inf = 0x3f3f3f3f;
const int N = 2e4 + 5;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , ans = inf , minv[N] , mins[N];
//所有面积和体积都是少乘一个π的 这样方便计算 对答案也没有影响
void dfs ( int stp , int lstr , int lsth , int s , int v )
{
if ( stp == 0 )
{
if ( v == n ) ans = min ( ans , s );
return;
}
if ( s + mins[stp] > ans ) return;
if ( v + minv[stp] > n ) return;
if ( 2 * ( n - v ) / lstr + s >= ans ) return;
for ( int i = lstr - 1 ; i >= stp ; i -- )
{
if ( stp == m ) s = i * i;
int maxhei = min ( lsth - 1 , ( n - minv[stp-1] - v ) / i / i );//最大高度:(总体积-上面层的最大体积0-下面层已经用过的体积)/底面积i*i
for ( int j = maxhei ; j >= stp ; j -- )
dfs ( stp - 1 , i , j , s + 2 * i * j , v + i * i * j );
}
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 ; i <= m ; i ++ ) minv[i] = minv[i-1] + i * i * i , mins[i] = mins[i-1] + 2 * i * i;
//体积是πr*r*r 表面积(侧面积)是2πr*r
//这里的1-m层是上到下的1-m层
//第i层半径至少为i,高度至少为i(因为是正整数
dfs ( m , n , n , 0 , 0 );
if ( ans == inf ) cout << 0 << endl;
else cout << ans << endl;
return 0;
}
E. 2.最大费用
观察数据范围 如果\(2^{40}\)会炸 所以考虑折半搜索 先搜出来前一半的所有价值组合 再搜后一半的 再用lower_bound将两部分合并 取得小于等于\(m\)的最大值
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid ((l+r)>>1)
#define int long long
const int N = (1<<20) + 5;
const int inf = INT_MAX;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , L;
int a[N] , cnt[2] , val[2][N];//前一半的所有价值组合 后一半的所有价值组合
void dfs ( int num , int st , int ed , int sum )
{
if ( st > ed ) return val[num][++cnt[num]] = sum , void();
dfs ( num , st + 1 , ed , sum ) , dfs ( num , st + 1 , ed , sum + a[st] );
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
int m1 = n >> 1 , m2 = n - m1;
for ( int i = 1 ; i <= m1 ; i ++ ) a[i] = read();
dfs ( 0 , 1 , m1 , 0 );
for ( int i = 1 ; i <= m2 ; i ++ ) a[i] = read();
dfs ( 1 , 1 , m2 , 0 );
sort ( val[1] + 1 , val[1] + cnt[1] + 1 , [](const int a , const int b) { return a > b; } );
int ans = 0;
for ( int i = 1 ; i <= cnt[0] ; i ++ )
{
int x = lower_bound ( val[1] + 1 , val[1] + 1 + cnt[1] , m - val[0][i] , greater<int>() ) - val[1];
ans = max ( ans , val[0][i] + val[1][x] );
}
cout << ans << endl;
return 0;
}
F. 3.骑士精神
\(IDA*\)模板题 相比于普通搜索多了一个估价函数
这里的估价函数很简单 就是将当前状态和目标状态进行比对 对于不相同的点数进行计数 如果计数结果加上步数大于限制步数则进行剪枝 否则继续进行搜索
需要每次特判一下初始就达到目标的情况()
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
const int goal[7][7] = {
{ 0 , 0 , 0 , 0 , 0 , 0 },
{ 0 , 1 , 1 , 1 , 1 , 1 },
{ 0 , 0 , 1 , 1 , 1 , 1 },
{ 0 , 0 , 0 , 2 , 1 , 1 },
{ 0 , 0 , 0 , 0 , 0 , 1 },
{ 0 , 0 , 0 , 0 , 0 , 0 }
};
int dx[] = { 0 , 1 , 1 , 2 , 2 , -1 , -1 , -2 , -2 };
int dy[] = { 0 , 2 , -2 , 1 , -1 , 2 , -2 , 1 , -1 };
char s[6][6];
int a[6][6] , flag;
int check ()
{
int cnt = 0;
for ( int i = 1 ; i <= 5 ; i ++ )
for ( int j = 1 ; j <= 5 ; j ++ )
if ( a[i][j] != goal[i][j] ) cnt ++;
return cnt;
}
void dfs ( int stp , int x , int y , int lim )
{
if ( !check() ) return flag = 1 , void();
for ( int i = 1 ; i <= 8 ; i ++ )
{
int tx = x + dx[i] , ty = y + dy[i];
if ( tx < 1 || tx > 5 || ty < 1 || ty > 5 ) continue;
swap ( a[tx][ty] , a[x][y] );
if ( stp + check() <= lim ) dfs ( stp + 1 , tx , ty , lim ) ;
swap ( a[tx][ty] , a[x][y] );
}
}
char ch;
signed main ()
{
int T = read();
while ( T -- )
{
int xx , yy;
for ( int i = 1 ; i <= 5 ; i ++ )
for ( int j = 1 ; j <= 5 ; j ++ )
{
cin >> ch;
if ( ch != '*' ) a[i][j] = ch - '0';
else a[i][j] = 2 , xx = i , yy = j;
}
if ( !check() ) { cout << 0 << endl; continue; }
flag = 0;
for ( int i = 1 ; i <= 15 ; i ++ )
{
dfs ( 0 , xx , yy , i );
if ( flag ) { cout << i << endl; break; }
}
if ( !flag ) cout << -1 << endl;
}
return 0;
}