搜索专题

一.折半搜索

顾名思义,就是将搜索数据减为原先的一半,分两次搜。

原先复杂度 \(O(2^n)\),优化成 \(O(2^{n/2})\),非常可观。

但是分两次后应合并,一半难点在于如何合并。

例题:

luogu-P3067

思路:

对每个数,如果加入 A 集合,那么和加上这个数即可;加入 B 集合,在 A 集合中减去这个数即可;按兵不动,就啥也不干。

最后合并可以采取双指针,两边相加为 0 即可。

但是这样有可能会重复,我们可以用二进制数来表示每个位置选和不选的状态,最后在按位或比较一下即可。

注意有可能产奶量相同而奶牛不同,所以在前后相同时,应该将右指针回到原来位置。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

#define ll long long

using namespace std;

int n, a[28];

ll ans;

bool vis[1 << 21];

struct node {
	int x, f;
};

node suml[1 << 21], sumr[1 << 21];

int cntl, cntr;

inline bool cmpl(node a1, node b1) {
	return a1.x < b1.x; 
}

inline bool cmpr(node a1, node b1) {
	return a1.x > b1.x;
}

void dfs(int l, int r, node sum[], int &num, int addx, int addf) {
	if (l > r) {
		sum[++ num] = (node) {addx, addf};
		return ;
	}
	
	dfs(l + 1, r, sum, num, addx + a[l], addf + (1 << (l - 1)));
	dfs(l + 1, r, sum, num, addx - a[l], addf + (1 << (l - 1)));
	dfs(l + 1, r, sum, num, addx, addf);
}

int main() {
	scanf("%d", &n);
	
	for (int i = 1; i <= n; i ++)
		scanf("%d", &a[i]);
	
	int mid = n >> 1;
	
	dfs(1, mid, suml, cntl, 0, 0);
	dfs(mid + 1, n, sumr, cntr, 0, 0);
	
	sort(suml + 1, suml + 1 + cntl, cmpl);
	sort(sumr + 1, sumr + 1 + cntr, cmpr);
	
	int i = 1, j = 1;
	
	while (i <= cntl && j <= cntr) {
		while (sumr[j].x + suml[i].x > 0 && j <= cntr) j ++;
		
		int pos = j; 
		
		while (j <= cntr && sumr[j].x + suml[i].x == 0) {
			int tmp = sumr[j].f | suml[i].f; 
			
			if (! vis[tmp]) {
				vis[tmp] = 1;
				ans ++;
			}
			
			j ++;
		}
		
		if (i < cntl && suml[i].x == suml[i + 1].x) j = pos;
		
		i ++;
	}
	
	printf("%lld\n", ans - 1);
	
	return 0;
}

二.A*

你需要设置一个估价函数来优化你的搜索。

满足这个公式 \(f(n) = g(n) + h(n)\)

其中 \(f(n)\) 是当前节点的估价,\(g(n)\) 是当前实际用的价格,\(h(n)\) 是对未来的估价,但是却不可能实现。

显然估价函数得写得好,不然和爆搜一样。。。

例题:

luogu-P1379

你与目标差了 \(x\) 个,显然至少要 \(x\) 步才可以达到目标状态。

这就是 \(h(n)\) 了。

然后记得将一行数字改成三行三列的矩阵处理即可。

不过据说暴力 bfs 也能过(bushi

代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>
#include <map>
#include <queue>

using namespace std;

string s;
string ss = "123804765";

int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};

map <string, bool> vis;
map <string, int> dis;

struct node {
	int f, ans;
	string now;
	
	bool operator < (const node &x) const {
		return f > x.f;
	}
};

inline int h(string c) {
	int res = 0;
	
	for (int i = 0; i < 9; ++ i)
		if (ss[i] != c[i]) res ++;
	
	return res;
}

inline bool exist(int x, int y) {
	return x >= 1 && x <= 3 && y >= 1 && y <= 3;
}

inline void A_star() {
	priority_queue <node> q;
	
	q.push((node) {h(s), 0, s});
	
	vis[s] = 1, dis[s] = 0;
	
	while(! q.empty()) {
		node u = q.top();
		
		q.pop();
		
		//cout << u.now << endl;
		
		if (u.now == "123804765") {
			printf("%d\n", u.ans);
			return ;
		}
		
		int sx, sy, tmp1, tmp2;
		
		for (int i = 0; i < 9; ++ i) {
			if (u.now[i] - '0' == 0)
				sx = i / 3 + 1, sy = i % 3 + 1, tmp1 = i;
		}
		
		for (int i = 0; i < 4; ++ i) {
			int nx = sx + dx[i];
			int ny = sy + dy[i];
			
			if (! exist(nx, ny)) continue;
			
			tmp2 = (nx - 1) * 3 + ny - 1;
			
			swap(u.now[tmp1], u.now[tmp2]);

			if (! vis[u.now] || (vis[u.now] && u.ans + 1 < dis[u.now])) {
				dis[u.now] = u.ans + 1;
				vis[u.now] = 1;
				
				q.push((node) {h(u.now) + u.ans + 1, u.ans + 1, u.now});
			}
			
			swap(u.now[tmp1], u.now[tmp2]);
		}
	}
}

int main() {
	cin >> s;
	
	if (h(s) == 0) {
		printf("0\n");
		return 0;
	}

	A_star();
	
	return 0;
}

三.IDA*

其实要分成两个部分,

迭代加深和 A*。

A* 很熟悉,迭代加深浅浅提一下。

适用条件:题目说至少,最少的或者在规定上限的情况下可尝试使用。

具体就是在每次搜索前设置一个最大深度使每次搜索的深度不超过这玩意。

那么在每次搜索运用上 A* 就是 IDA* 了。

例题:

luogu-P2534

发现求的是最少的步数,可以使用 IDA*。

那么估价函数怎么写?

若是我们前后高度并未按照从小到大的顺序排的时候,至少需要一次旋转,即从头到此位置的一次旋转才可以让这两个盘按照从小到大的顺序。

具体实现的时候,考虑将数组离散化处理,并且记得判断最后一个盘。

代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;

int n;

int a[20], goal[20];

inline int h() {
	int res = 0;
	
	for (int i = 1; i <= n; i ++)
		if (abs(a[i + 1] - a[i]) != 1) res ++;
	
	return res;
}

inline void reserve(int num) {
	for (int i = 1; i <= (num >> 1); ++ i)
		swap(a[i], a[num + 1 - i]);
}

bool A_star(int step, int up, bool flag) {
	if (step == up)
		return h() == 0 ? 1 : 0;
	
	for (int i = 1; i <= n; ++ i) {
		reserve(i);
		
		if (step + h() <= up) 
			flag = A_star(step + 1, up, flag);
			
		if (flag) break;
		
		reserve(i);
	}
	
	return flag;
}

int main() {
	scanf("%d", &n);
	
	for (int i = 1; i <= n; ++ i) {
		scanf("%d", &a[i]);
		
		goal[i] = a[i];
	}
	
	sort(goal + 1, goal + 1 + n);
	
	for (int i = 1; i <= n; i ++)
		a[i] = lower_bound(goal + 1, goal + 1 + n, a[i]) - goal;//离散化
	
	a[n + 1] = n + 1;
	
	if (! h()) {
		printf("0\n");
		return 0;
	}
	
	int maxdep = 1;
	
	while (! A_star(0, maxdep, 0)) maxdep ++;
	
	printf("%d\n", maxdep);
	
	return 0;
}

四.舞蹈链(DLX)

咕了好久,主要是脑子笨学不会。

舞蹈链主要用来解决精确覆盖问题,详情参考洛谷P4929

爆搜显然会超时,于是某位佬就发明了这个算法。

具体操作步骤如下:

1.建立一个二维双向链表,对于每个 1 进行插入,由于是二维双向,所以会有上下左右的元素,当然同时也要记录这个数据在原来的数组的地方。

2.移除操作,我们移除了第 c 列,显然,第 c 列左边的元素的右边元素变成了第 c 列右边的元素,第 c 列右边的元素的左边元素变成了第 c 列左边的元素,随即删除这列下来包括的 1 所在位置所包括的行,同样的,被扫到元素 i 的上面的下面的元素变成了 i 的下面,i 的下面的上面的元素变成了 i 的上面。

3.恢复操作,因为我们在插入操作中记录了改列在原数组所在的地方,所以和移除操作相反。

4.当我们所有的 1 都被清除干净且没有多余的列剩下时,即为成功,否则进行 2,3 两步。

P.S:每次删除,优先选择 1 最多的列删除,减少删除次数。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>

const int N = 5507;

using namespace std;

int n, m;

int cnt, lin[N], sz[N];//链中的第cnt个元素,第i行的第一个数,第j列的元素个数。

struct direction {
	int l, r, u, d;//第i元素的左右上下元素的指针
} dir[N];

struct position {
	int row, col;//第i个元素在row[i]行,col[i]列
} pos[N];

int ans[507];

struct Dancing_Link {
	inline void build() {
		for (int i = 0; i <= m; ++ i) 
			dir[i] = (direction) {i - 1, i + 1, i, i};
			
		dir[0].l = m, dir[m].r = 0, cnt = m;//构成双向链表,矩阵中有了m个元素
	}
	
	inline void insert(int ro, int co) {
		pos[++ cnt] = (position) {ro, co};//第cnt元素所在的行列
		sz[co] ++;//这个元素所在的列的元素个数加一
		
		dir[cnt].u = co, dir[cnt].d = dir[co].d;
		dir[dir[co].d].u = cnt, dir[co].d = cnt;//将第cnt个元素加入纵向链表
		
		if (lin[ro] == 0) {
			lin[ro] = cnt;
			dir[cnt].l = cnt, dir[cnt].r = cnt;
		} else {
			dir[cnt].l = lin[ro], dir[cnt].r = dir[lin[ro]].r;
			dir[dir[lin[ro]].r].l = cnt, dir[lin[ro]].r = cnt;
		}//将第cnt个元素插入横向链表
	}
	
	inline void remove(int c) {//移除第c列
		dir[dir[c].l].r = dir[c].r;
		dir[dir[c].r].l = dir[c].l;
		
		for (int i = dir[c].d; i != c; i = dir[i].d) {//顺着
			for (int j = dir[i].r; j != i; j = dir[j].r) {
				dir[dir[j].d].u = dir[j].u;
				dir[dir[j].u].d = dir[j].d;
				sz[pos[j].col] --;
			}
		}
	}
	
	inline void recover(int c) {//恢复第c列
		for (int i = dir[c].u; i != c; i = dir[i].u) {
			for (int j = dir[i].l; j != i; j = dir[j].l) {
				dir[dir[j].d].u = j;
				dir[dir[j].u].d = j;
				sz[pos[j].col] ++;
			}
		}
		
		dir[dir[c].l].r = c;
		dir[dir[c].r].l = c;
	}
} dlx;

inline void print(int num) {
	for (int i = 1; i < num; ++ i)
		printf("%d ", ans[i]);
	printf("\n");
}

bool dfs(int step) {//step即为删除的列的个数
	if (dir[0].r == 0) {
		print(step);
		return 1;
	}
	
	int c = dir[0].r;
	
	for (int i = dir[0].r; i; i = dir[i].r)
		if (sz[i] < sz[c]) c = i;
	
	dlx.remove(c);
	
	for (int i = dir[c].d; i != c; i = dir[i].d) {
		ans[step] = pos[i].row;
		
		for (int j = dir[i].r; j != i; j = dir[j].r)
			dlx.remove(pos[j].col);
		
		if (dfs(step + 1)) return 1;
		
		for (int j = dir[i].l; j != i; j = dir[j].l) 
			dlx.recover(pos[j].col);
	}
	
	dlx.recover(c);
	
	return 0;
}

int main() {
	scanf("%d %d", &n, &m);
	
	dlx.build();
	
	for (int i = 1; i <= n; ++ i) {
		for (int j = 1; j <= m; ++ j) {
			int x;
			
			scanf("%d", &x);
			
			if (x) dlx.insert(i, j);
		}
	}
	
	if (! dfs(1)) printf("No Solution!\n");
	
	return 0;
}

因为很少有题目出的如此白痴,舞蹈链更多的用来解决数独问题。

类似于这道:洛谷P1784

这边提一嘴怎么建立矩阵:

考虑列:每个格子都要有一个数字,这列出 81 列;每一行里面一共有 9 个格子,共有 9 行,又可以列出 81 列;每一列里面一共有 9 个格子,共有 9 列,又可以列出 81 列;每一个宫格里有 9 个格子,有 9 个宫格,列出 81 列;总计 324 列。

考虑行:每个格子都得有一个数字,每个格子都有 9 种情况,总共是 729 行。

代码不放了,太过冗长和丑陋。

搜索专题总算结束了。

posted @ 2022-12-24 19:37  Kalium  阅读(55)  评论(0)    收藏  举报