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查询时使用

对于每一个字母 枚举所有没被使用过的数字即可

接下来考虑剪枝 从前往后考虑每一列三个数(先将这三个数提取出来)

  1. 三个数都确定

    • 进位也确定:当且仅当上一列三个数都是确定的 可以计算出来进位是0/1 那么判断\((x+y+jin)\% n\)是否为\(z\) 再判断最高位不能有进位 做出剪枝
    • 进位不确定:上一列三个数不全确定 那么如果$( x + y ) % n != z \(且\) ( x + y + 1 ) % n != z$ 就是不符合情况的 再判断最高位不能有进位
  2. 三个数不确定:

    直接令\(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.生日蛋糕

剪枝有三种:

  1. 可行性剪枝 后面的最小体积加上当前体积如果超过限制就返回
  2. 最优性剪枝 后面的最小表面积加上当前表面积如果更劣就返回
  3. 最优性剪枝 将体积和表面积综合起来 算出后面的理论最小表面积 如果更劣就返回

这里详细推一下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;
}
posted @ 2023-06-23 20:07  Echo_Long  阅读(174)  评论(0编辑  收藏  举报