深搜,就是在每个点 x 上面对多条分支时,任意选择一条边走下去,执行递归,直至回溯到点 x 后,再考虑走其他的边。
这个很简单,不写了
优化搜索顺序
大部分情况下,我们应该优先搜索分支较少的节点。
排除等效冗余
在搜索的时候,尽量不搜索重复的状态,即在不考虑顺序的时候,尽量用组合的方式来搜索。
可行性剪枝
搜索的一半的时候,发现不合法就可以退出。
最优性剪枝
搜索到某一个程度的时候,如果发现当前的状态无论如何都比之前搜索到的最优解差,那就可以退出。
记忆化搜索(DP)
题目描述:
翰翰和达达饲养了 N N N 只小猫,这天,小猫们要去爬山。
经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。
翰翰和达达只好花钱让它们坐索道下山。
索道上的缆车最大承重量为 W W W,而 N N N 只小猫的重量分别是 C 1 、 C 2 … … C N C_1、C_2……C_N C1、C2……CN。
当然,每辆缆车上的小猫的重量之和不能超过 W W W。
每租用一辆缆车,翰翰和达达就要付 1 1 1 美元,所以他们想知道,最少需要付多少美元才能把这 N N N 只小猫都运送下山?
思路:
对于这题,我们如果用深搜来做,可以考虑依次把每只小猫分配到一辆已经租好的缆车上,或者新租一辆给小猫。
此时,我们只关心三个状态:
根据这三个状态,我们可以考虑编写一个 d f s ( n o w , c n t ) dfs(now, cnt) dfs(now,cnt) 函数,其中 n o w now now 代表当前即将运送的小猫, c n t cnt cnt 代表已经租用的缆车数量,还可以写一个 c a b cab cab 数组来存储每辆缆车上小猫的重量之和。
n o w now now、 c n t cnt cnt 和 c a b cab cab 数组共同标识着问题状态空间所类比的“图”中的一个“节点”。在这个“节点”上,至多有 c n t + 1 cnt + 1 cnt+1 个可能的分支。
于是,我们可以只考虑当前这个 n o w now now 小猫是否能够被分配到已租用的缆车上,条件为 c a b [ i ] + c [ n o w ] ≤ W cab[i] + c[now] \le W cab[i]+c[now]≤W 。如果不能,那么就多租一辆来分配这个小猫, c a b [ c n t + 1 ] = c [ n o w ] cab[cnt + 1] = c[now] cab[cnt+1]=c[now] 。
为了减少搜索树“分支”的数量,我们可以在搜索前,先把小猫按照重量从大到小递减排序,优先搜索重量较大的小猫。
#include
#include
#include
using namespace std;
const int N = 20;
int n, w;
int c[N], cab[N];
int ans = 0;
bool cmp(int a, int b)
{
return a > b;
}
void dfs(int now, int cnt)
{
if(cnt >= ans) return ;
if(now == n) {
ans = min(ans, cnt);
return ;
}
for(int i = 1; i <= cnt; i++)
if(c[now] + cab[i] <= w) {
cab[i] += c[now];
dfs(now + 1, cnt);
cab[i] -= c[now];
}
cab[cnt + 1] = c[now];
dfs(now + 1, cnt + 1);
cab[cnt + 1] = 0;
return ;
}
int main()
{
cin >> n >> w;
for(int i = 0; i < n; i++) cin >> c[i];
sort(c, c + n, cmp);
ans = 0x3f3f3f3f;
dfs(0, 0);
cout << ans << endl;
return 0;
}
题目描述:
数独是一种传统益智游戏,你需要把一个 9 × 9 9×9 9×9 的数独补充完整,使得图中每行、每列、每个 3 × 3 3×3 3×3 的九宫格内数字 1 ∼ 9 1∼9 1∼9 均恰好出现一次。
请编写一个程序填写数独。
输入格式
输入包含多组测试用例。
每个测试用例占一行,包含 81 81 81 个字符,代表数独的 81 81 81 个格内数据(顺序总体由上到下,同行由左到右)。
每个字符都是一个数字( 1 − 9 1−9 1−9)或一个 .
(表示尚未填充)。
您可以假设输入中的每个谜题都只有一个解决方案。
文件结尾处为包含单词 end
的单行,表示输入结束。
输出格式
每个测试用例,输出一行数据,代表填充完全后的数独。
输入样例:
4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end
输出样例:
417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936
思路
本题的“搜索树”规模是非常大的,如果直接随便找一个可以填的位置就去搜索的话,肯定是不行的。
于是这里考虑一个剪枝,就是选择“能填的合法数字”最少的位置,并且考虑该位置应填上什么数,作为搜索分支。
但是对于这样一长串的数组,如果直接搜索也是不好做的,可以考虑位运算来代替数组执行“对数独各个位置所填数字的记录”以及“可填性的检查与统计”,也就是进行了常数优化。
代码实现
#include
#include
#include
using namespace std;
const int N = 9, M = 1 << N;
int ones[M], map[M];
int row[N], col[N], cell[N][N];
char str[100];
//取出最末尾的1,取出来的数是一个十进制的整数。
inline int lowbit(int x)
{
return x & -x;
}
//将行、列和3*3九宫格里的状态都做一次按位与运算,得出哪些位置上可以放数字。
inline int get(int x, int y)
{
return row[x] & col[y] & cell[x / 3][y / 3];
}
//初始化,最开始的时候所有状态都是全1,表示都可以放置数字
void init()
{
for(int i = 0; i < N; i++) row[i] = col[i] = M - 1;
for(int i = 0; i < N; i++)
for(int j = 0; j < N; j++)
cell[i][j] = M - 1;
}
//将(x,y)填上对应的数字 1 + t
//is_set 为 true 表示填数字,false 表示还原成 '.'
void draw(int x, int y, int t, bool is_set)
{
if(is_set) str[x * N + y] = '1' + t;
else str[x * N + y] = '.';
int v = 1 << t;
if(!is_set) v = -v;
row[x] -= v;
col[y] -= v;
cell[x / 3][y / 3] -= v;
}
bool dfs(int cnt)
{
if(!cnt) return true;
//找到可填数最少的位置。
int minv = 10;
int x, y;
for(int i = 0; i < N; i ++)
for(int j = 0; j < N; j++)
if(str[i * N + j] == '.')
{
int t = ones[get(i, j)];
if(t < minv)
{
minv = t;
x = i, y = j;
}
}
//分别搜索每一个可填数
int state = get(x, y);
for(int i = state; i; i -= lowbit(i))
{
int t = map[lowbit(i)];
draw(x, y, t, true);
if(dfs(cnt - 1)) return true;
draw(x, y, t, false);
}
return false;
}
int main()
{
//ones[i]表示数i中有几个1
for(int i = 0; i < M; i++)
for(int j = 0; j < N; j++)
ones[i] += i >> j & 1;
//将数 10^i 中1的位置预处理出来
for(int i = 0; i < N; i++) map[1 << i] = i;
while(scanf("%s", str) != EOF && str[0] != 'e')
{
init();
int cnt = 0;
for(int i = 0, k = 0; i < N; i++)
for(int j = 0; j < N; j++, k++)
if(str[k] != '.')
{
int t = str[k] - '1';
draw(i, j, t, true);
}
else cnt ++;
dfs(cnt);
printf("%s\n", str);
}
return 0;
}
题目描述
乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 5050 个长度单位。
然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
请你设计一个程序,帮助乔治计算木棒的可能最小长度。
每一节木棍的长度都用大于零的整数表示。
输入格式
输入包含多组数据,每组数据包括两行。
第一行是一个不超过 6464 的整数,表示砍断之后共有多少节木棍。
第二行是截断以后,所得到的各节木棍的长度。
在最后一组数据之后,是一个零。
输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。
数据范围
数据保证每一节木棍的长度均不大于 5050。
输入样例:
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
输出样例:
6
5
代码实现
#include
#include
#include
using namespace std;
const int N = 65;
int n;
int l[N], sum, length;
bool st[N];
bool dfs(int u, int s, int start)
{
if(u * length == sum) return true;
if(s == length) return dfs(u + 1, 0, 0);
for(int i = start; i < n; i++)
{
if(st[i]) continue;
if(s + l[i] > length) continue;
st[i] = true;
if(dfs(u, s + l[i], i + 1)) return true;
st[i] = false;
if(!s) return false;
if(s + l[i] == length) return false;
int j = i;
while(j < n && l[i] == l[j]) j++;
i = j - 1;
}
return false;
}
int main()
{
while(cin >> n, n)
{
memset(st, false, sizeof st);
sum = 0;
for(int i = 0; i < n; i++)
{
cin >> l[i];
sum += l[i];
}
sort(l, l + n);
reverse(l, l + n);
length = 1;
while(true)
{
if(sum % length == 0 && dfs(0, 0, 0))
{
cout << length << endl;
break;
}
length ++;
if(length > sum) break;
}
}
return 0;
}
题目描述:
7月17日是 M r . W Mr.W Mr.W 的生日, A C M − T H U ACM-THU ACM−THU 为此要制作一个体积为 N π N_π Nπ 的 M M M 层生日蛋糕,每层都是一个圆柱体。
设从下往上数第 i ( 1 ≤ i ≤ M ) i(1 \le i \le M) i(1≤i≤M) 层蛋糕是半径为 R i R_i Ri , 高度为 H i H_i Hi 的圆柱。当 i < M i < M i<M 时,要求 R i > R i + 1 R_i > R_{i+1} Ri>Ri+1 且 H i > H i + 1 H_i > H_{i+1} Hi>Hi+1。
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q Q Q 最小。
令 Q = S π Q = Sπ Q=Sπ
请编程对给出的 N N N 和 M M M ,找出蛋糕的制作方案(适当的 R i R_i Ri 和 H i H_i Hi 的值),使 S S S 最小。
(除 Q Q Q 外,以上所有数据皆为正整数)
思路:
本题没什么难以理解的。
这里设最顶上一层为第 1 1 1 层,共有 M M M 层,总体积为 N N N ,且有 R i < R i + 1 , H i < H i + 1 R_i
剪枝策略
上下界剪枝:
u ≤ R [ u ] ≤ m i n ( R [ u + 1 ] − 1 , n − v ) u ≤ H [ u ] ≤ m i n ( H [ u + 1 ] − 1 , u − v R [ u ] 2 ) u\le R[u] \le min(R[u + 1]-1, \sqrt{n - v}) \\ u\le H[u] \le min(H[u+1]-1, \cfrac{u-v}{R[u]^2}) u≤R[u]≤min(R[u+1]−1,n−v)u≤H[u]≤min(H[u+1]−1,R[u]2u−v)
证明:
由于每一层 R R R 和 H H H 的大小都是严格递增的,并且都为正整数,所以最小也只能是第 u u u 层的层数。
对于体积 V u = π R u 2 H u = n − v V_u=\pi R_u^2 H_u = n-v Vu=πRu2Hu=n−v
所以,当 H u H_u Hu 取 1 1 1 时, R u 2 R_u^2 Ru2 可以取到最大。因此 R u ≤ m i n ( R u + 1 − 1 , n − v ) R_u\le min(R_{u+1}-1, \sqrt{n-v}) Ru≤min(Ru+1−1,n−v)。
对于 H u H_u Hu 也是同理。
优化搜索顺序:
每一层的 R R R 和 H H H 都从大到小枚举,减少分支数量。
可行性/最优性剪枝:
设 m i n v [ u ] minv[u] minv[u] 代表第 u u u 层前最小的体积之和, m i n s [ u ] mins[u] mins[u] 代表第 u u u 层前最小的面积之和。
有 v + m i n v [ u ] ≤ n s + m i n s [ u ] < a n s v+minv[u] \le n \\ s+mins[u] < ans v+minv[u]≤ns+mins[u]<ans
还有一个最优性剪枝,比较难想,这里需要一定的证明。
利用 R R R 数组和 H H H 数组,1~u 层的体积可表示为 n − v = ∑ k = 1 u R [ k ] 2 H [ k ] n-v=\sum_{k=1}^{u}R[k]^2H[k] n−v=∑k=1uR[k]2H[k],表面积可表示为 S u = ∑ k = 1 u 2 R [ k ] H [ k ] S_u=\sum_{k=1}^u2R[k]H[k] Su=∑k=1u2R[k]H[k]。
其中, S u = 2 ∑ k = 1 u R [ k ] H [ k ] = 2 R [ u + 1 ] ∑ k = 1 u R [ k ] H [ k ] R [ u + 1 ] > 2 R [ u + 1 ] ∑ k = 1 u R [ k ] 2 H [ k ] S_u=2\sum_{k=1}^uR[k]H[k]= \cfrac{2}{R[u+1]}\sum_{k=1}^uR[k]H[k]R[u+1]>\cfrac{2}{R[u+1]}\sum_{k=1}^uR[k]^2H[k] Su=2∑k=1uR[k]H[k]=R[u+1]2∑k=1uR[k]H[k]R[u+1]>R[u+1]2∑k=1uR[k]2H[k]
所以, S u > 2 ( n − v ) R [ u + 1 ] S_u>\cfrac{2(n-v)}{R[u+1]} Su>R[u+1]2(n−v)
因此,当 s + S u = s + 2 ( n − v ) R [ u + 1 ] ≥ a n s s+S_u=s+\cfrac{2(n-v)}{R[u+1]} \ge ans s+Su=s+R[u+1]2(n−v)≥ans 的时候,就可以剪枝了。
代码实现:
#include
#include
#include
#include
#include
using namespace std;
const int N = 25;
const int INF = 1e9;
int n, m;
int minv[N], mins[N];
int R[N], H[N];
int ans = INF;
void dfs(int u, int v, int s)
{
if(v + minv[u] > n) return ;
if(s + mins[u] >= ans) return ;
if(s + 2 * (n - v) / R[u + 1] >= ans) return ;
if(!u)
{
if(v == n) ans = s;
return ;
}
for(int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r--)
for(int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h--)
{
int t = 0;
if(u == m) t = r * r;
R[u] = r, H[u] = h;
dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
}
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1; i <= m; i++)
{
minv[i] = minv[i - 1] + i * i * i;
mins[i] = mins[i - 1] + 2 * i * i;
}
R[m + 1] = H[m + 1] = INF;
dfs(m, 0, 0);
if(ans == 1e9) ans = 0;
printf("%d\n", ans);
return 0;
}
#include
#include
using namespace std;
const int N = 110;
int n;
int path[N];
bool dfs(int u, int depth)
{
if(u > depth) return false;
if(path[u - 1] == n) return true;
bool st[N] = {0};
for(int i = u - 1; i >= 0; i--)
for(int j = i; j >= 0; j--)
{
int s = path[i] + path[j];
if(s > n || st[s] || s <= path[u - 1]) continue;
st[s] = true;
path[u] = s;
if(dfs(u + 1, depth)) return true;
}
return false;
}
int main()
{
path[0] = 1;
while(cin >> n, n)
{
int depth = 1;
while(!dfs(1, depth)) depth ++;
for(int i = 0; i < depth; i++) cout << path[i] << " ";
cout << endl;
}
return 0;
}
题目描述
达达帮翰翰给女生送礼物,翰翰一共准备了 NN 个礼物,其中第 ii 个礼物的重量是 G[i]G[i]。
达达的力气很大,他一次可以搬动重量之和不超过 WW 的任意多个物品。
达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。
输入格式
第一行两个整数,分别代表 W W W 和 N N N。
以后 N N N 行,每行一个正整数表示 G [ i ] G[i] G[i]。
输出格式
仅一个整数,表示达达在他的力气范围内一次性能搬动的最大重量。
数据范围
1 ≤ N ≤ 46 , 1 ≤ W , G [ i ] ≤ 2 31 − 1 1≤N≤46, 1≤W,G[i]≤2^{31}−1 1≤N≤46,1≤W,G[i]≤231−1
输入样例:
20 5
7
5
4
18
1
输出样例:
19
代码实现
#include
#include
#include
#include
using namespace std;
typedef long long LL;
const int N = 50;
int n, m;
int w[N], k;
int weight[1 << 25], cnt;
int ans;
void dfs1(int u, int s)
{
if(u == k)
{
weight[cnt ++] = s;
return ;
}
dfs1(u + 1, s);
if((LL)s + w[u] <= m) dfs1(u + 1, s + w[u]);
}
void dfs2(int u, int s)
{
if(u == n)
{
int l = 0, r = cnt - 1;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(weight[mid] <= m - s) l = mid;
else r = mid - 1;
}
ans = max(ans, weight[l] + s);
return ;
}
dfs2(u + 1, s);
if((LL)s + w[u] <= m) dfs2(u + 1, s + w[u]);
}
int main()
{
cin >> m >> n;
for(int i = 0; i < n; i++) cin >> w[i];
sort(w, w + n);
reverse(w, w + n);
k = n / 2 + 2;
dfs1(0, 0);
sort(weight, weight + cnt);
cnt = unique(weight, weight + cnt) - weight;
dfs2(k, 0);
cout << ans << endl;
return 0;
}
通常,连通性模型其实dfs和bfs都是可以做的,其中区别就是“bfs在搜索状态的过程中,可以记录到达每个点的最短距离,而dfs只能确认是否能够到达”,由于dfs实现较快,所以也常采用dfs来解决一系列连通性的问题。
题目描述:
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n ∗ n n∗n n∗n 的格点组成,每个格点只有2种状态,.
和#
,前者表示可以通行后者表示不能通行。
同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。
如果起点或者终点有一个不能通行(为#),则看成无法办到。
注意:A、B不一定是两个不同的点。
代码实现:
#include
#include
#include
#include
using namespace std;
const int N = 110;
int n;
char g[N][N];
bool st[N][N];
int sx, sy, ex, ey;
int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};
bool dfs(int x, int y)
{
if(g[x][y] == '#') return false;
if(x == ex && y == ey) return true;
st[x][y] = true;
for(int i = 0; i < 4; i++)
{
int tx = x + dx[i], ty = y + dy[i];
if(tx < 0 || tx >= n || ty < 0 || ty >= n) continue;
if(st[tx][ty]) continue;
if(dfs(tx, ty)) return true;
}
return false;
}
int main()
{
int t;
scanf("%d", &t);
while(t--)
{
memset(st, false, sizeof st);
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%s", g[i]);
scanf("%d %d %d %d", &sx, &sy, &ex, &ey);
if(dfs(sx, sy)) puts("YES");
else puts("NO");
}
return 0;
}
题目描述:
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。
你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。
请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
其实这题也就类似与 F l o o d − F i l l Flood-Fill Flood−Fill 。
代码实现:
#include
#define sc scanf
#define pf printf
using namespace std;
typedef pair PII;
const int N = 25;
int n, m, cnt;
char mp[N][N];
bool vis[N][N];
void dfs(int x, int y)
{
if(x < 0 || x >= n || y < 0 || y >= m) return ;
if((mp[x][y] == '.' || mp[x][y] == '@') && !vis[x][y]) {
cnt++;
vis[x][y] = true;
dfs(x - 1, y);
dfs(x + 1, y);
dfs(x, y - 1);
dfs(x, y + 1);
}
}
int main()
{
while(cin >> m >> n && n && m)
{
memset(vis, false, sizeof vis);
cnt = 0;
int stx = 0, sty = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
{
cin >> mp[i][j];
if(mp[i][j] == '@')
{
stx = i;
sty = j;
}
}
// vis[stx][sty] = true;
dfs(stx, sty);
cout << cnt << endl;
}
return 0;
}