7.4.2其他应用举例
笔者偷懒了一会儿,又回来补题解了
Prime_Ring_Problem:
很朴素的一道题目,注意剪枝即可(否则16!很容易TLE,MLE),判断条件就是相邻两数之和不是素数就返回
点击查看笔者代码
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 16 + 10;
int n;
bool dis[maxn], isprime[maxn*2];
deque<int> ans;
void deal() {
memset(isprime, 1, sizeof(isprime));
for(int i = 2; i < 32; i++)
for(int j = 2; j < i; j++)
if(!(i%j)) { isprime[i] = false; break; }
}
void dfs(int s, int pos) {
if(pos == n) {
if(isprime[s+1]) {
deque<int> output = ans;
while(!output.empty()) {
printf("%d ", output.front());
output.pop_front();
}
printf("%d\n", s);
}
return;
}
ans.push_back(s); dis[s] = true;
for(int i = 1; i <= n; i++)
if(!dis[i] && isprime[i+s]) dfs(i, pos+1);
ans.pop_back(); dis[s] = false;
}
int main() {
int kase = 0;
deal();
while(scanf("%d", &n) == 1) {
if(kase) cout << endl;
printf("Case %d:\n", ++kase);
memset(dis, 0, sizeof(dis));
dfs(1, 1);
}
return 0;
}
点击查看作者代码
for(int i = 2; i <= n*2; i++) isp[i] = is_prime(i); //生成素数表,加快后续判断
for(int i = 0; i < n; i++) A[i] = i+1;
do {
int ok = 1;
for(int i = 0; i < n; i++) if(!isp[A[i]+A[(i+1)%n]]) { ok = 0; break; }
//判断合法性
if(ok) {
for(int i = 0; i < n; i++) printf("%d ", A[i]); //输出序列
printf("\n");
}
} while(next_permutation(A+1, A+n)); //1的位置不变
/*
这是直接枚举所有情况,当n=12时就很慢了
*/
void dfs(int cur) {
if(cur == n && isp[A(0)+A[n-1]]) {//递归边界,别忘了测试第一个数和最后一个数
for(int i = 0; i < n; i++) printf("%d ", A[i]); //打印方案
printf("\n");
}
else for(int i = 2; i <= n; i++) //尝试放置每个数
if(!vis[i] && isp[i+A[cur-1]]) {//如果i没有用过,并且与前一个数之和为素数
A[cur] = i;
vis[i] = 1; //设置使用标志
dfs(cur+1);
vis[i] = 0; //清楚标志
}
}
/*
从解答树的角度讲,回溯法正是按照深度优先的顺序在遍历解答树
如果最坏情况下的枚举量很大,应该使用回溯法,而不是生成-测试法
*/
Krypton_Factor:
本题就是通过dfs的特性从0开始枚举,注意开头不一定都是A,6 2 的答案是BAB,这个样例需要注意,因此我们需要维护ans字符数组的长度
因为本题长度只有80,直接暴力判断是否存在容易的串就可以了,接着就是简单的全排列弱化版代码即可
点击查看笔者代码
#include<iostream>
#include<cstring>
using namespace std;
const int maxn = 80 + 5;
char ans[maxn];
int n, l, cnt = 0;
void output() {
int len = strlen(ans);
for(int i = 0; i < len; i++) {
cout << ans[i];
if(i==len-1) { cout << endl; break; }
if(i%4==3 && i%64!=63) cout << " ";
else if(i%64 == 63) cout << endl;
}
cout << len << endl;
}
bool check(int len) {
for(int i = 2; i <= len/2; i++) {
for(int j = 0, k = i; k < len; j++, k++) {
bool flag = true;
for(int l = 0; l < i; l++)
if(ans[j+l] != ans[k+l]) { flag = false; break; }
if(flag) return false;
}
}
return true;
}
void dfs(int pos) {
if(cnt == n) return;
for(int i = 0; i < l; i++) {
if(!pos || ans[pos-1]!='A'+i) {
ans[pos] = 'A' + i;
if(check(pos+1)) {
cnt++;
dfs(pos+1);
}
if(cnt==n) return;
ans[pos] = '\0';
}
}
}
int main() {
while(scanf("%d%d", &n, &l) == 2 && n && l) {
memset(ans, 0, sizeof(ans));
cnt = 0;
dfs(0);
output();
}
return 0;
}
Bandwidth
这边因为数据过小,可以说是为暴力搜索而生的,因此本题不进行回溯在时间上也不会受到很大的限制,这边笔者只进行了简单的回溯(只要是懒得想了)如果对于第i点他的带宽大于前面的已有的最小带宽,那么这就是无效路径,返回上一层
点击查看笔者代码
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 26 + 5, maxl = 10;
int cnt, width, sign[maxn], ch[maxl], tch[maxl], ans[maxl];
bool vis[maxn], G[maxn][maxn];
bool getD() {
string line;
cin >> line;
memset(G, 0, sizeof(G));
memset(vis, 0, sizeof(vis));
memset(sign, 0, sizeof(sign));
memset(ch, 0, sizeof(ch));
memset(tch, 0, sizeof(tch));
memset(ans, 0, sizeof(ans));
if(line=="#") return false;
while(line.find(':') != string::npos) {
int temp = line.find(':');
int from = line[temp-1] - 'A';
vis[from] = true;
for(int i = temp+1; i < line.length(); i++) {
if(line[i]==';') break;
int to = line[i] - 'A';
vis[to] = true;
G[from][to] = G[to][from] = true;
}
line = line.substr(temp+1);
}
return true;
}
int getW() {
int output = 0;
for(int i = 0; i < cnt; i++)
for(int j = 0; j < cnt; j++)
if(G[ch[i]][ch[j]]) output = max(output, abs(sign[ch[i]]-sign[ch[j]]));
return output;
}
void dfs(int cur) {
if(cur == cnt) {
int w = getW();
if(width > w) {
width = w;
for(int i = 0; i < cnt; i++) ans[i] = tch[i];
}
}
for(int i = 0; i < cnt; i++) {
if(!vis[i]) {
vis[i] = true; sign[ch[i]] = cur;
if(cur > width) {
bool flag = false;
for(int j = 0; j < cur-width; j++) if(G[ch[tch[j]]][ch[i]]) { flag = true; break; }
if(flag) { vis[i] = false; continue; }
}
tch[cur] = i;//tch store cur ch store pre-array
dfs(cur+1);
vis[i] = false;
}
}
}
int main() {
while(getD()) {
cnt = 0; width = 10000;
for(int i = 0; i < 26; i++) if(vis[i]) ch[cnt++] = i;
memset(vis, 0, sizeof(vis));
dfs(0);
for(int i = 0; i < cnt; i++) cout << (char)(ch[ans[i]]+'A') << ' ';
cout << "-> " << width << endl;
}
return 0;
}
作者的分析是:和八皇后问题不同的是,八皇后问题有很多可行性约束(feasibility constraint),可以在得到完整解之前避免扩展那些不可行的点,但本体并没有可行性约束,也就是说任何排列都是合法的,难道只能扩展所有的结点吗
答案是否定的,笔者认为除了可行性约束之外,还才在局部最优性约束,简单来讲如果当前带宽大于之前带宽的最小值,那么这就是一个不必要的展开
可以记录下目前已经找到的最小带宽k,如果发现已经有某两个结点的距离大于或等于k,再怎么扩展也不可能比当前解更优,应当强制把它剪掉,即为解答树剪枝(prune)
除此之外,还可以剪掉更多的枝叶(笔者未曾设想的道路),如果在搜索到结点u时,u结点还有m个相邻点没有确定位置,那么对于结点u来说,最理想的情况就是这m个结点紧跟在u后面,这样结点的带宽为m,而在其他任何非理想情况的带宽至少为m+1.这样如果m>=k,即“在最理想的情况下都不能得到比当前最优解更好的方案”,应当剪枝
如果说最前面是由后向前的剪枝,那么后者则是从前向后的剪枝,他们的基础都是记忆化搜索
在求解最优解的文中,应尽量考虑最优性剪枝。这往往需要记录当下当前最优解。并且想办法“预测”以下从当前结点出发是否可以扩展到更好的方案。具体来说,先计算以下最理想情况可以得到怎样的解,如果连理想情况都无法得到比当前最优解更好的方案,则剪枝
Mobile Computing:
点击查看代码
#include<iostream>
#include<cstring>
#include<iomanip>
#include<vector>
using namespace std;
constexpr int maxn = 6 + 5, precision = 20;
struct Node{
int w;
Node *nleft, *nright;
Node(int w = 0, Node *nleft = NULL, Node* nright = NULL) : w(w), nleft(nleft), nright(nright) {}
} node[maxn];
struct Point{
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
} p[maxn*2];
int n, base[maxn] = {0, 0, 1, 3, 6, 10, 15};
double r, ans = -1;
void getD() {
memset(node, 0, sizeof(node));
cin >> r >> n;
ans = -1;
for(int i = 0; i < n; i++) cin >> node[i].w;
}
void getN(Node* root, double npos, double& nl, double& nr, int cur) {
Node* nleft = root->nleft;
Node* nright = root->nright;
double nowl = npos-(double)nright->w/(nright->w+nleft->w), nowr = npos+(double)nleft->w/(nleft->w+nright->w);
if(nl > nowl) nl = nowl;
if(nr < nowr) nr = nowr;
if(nr-nl > r) return;
if(nleft->nleft != NULL) getN(nleft, nowl, nl, nr, cur+1);
if(nright->nleft != NULL) getN(nright, nowr, nl, nr, cur+1);
if(!cur && nr-nl < r && nr-nl > ans) ans = nr-nl;
}
void getT(int cnt, vector<Node*> v) {
if(cnt == 1) {
double nl = 0, nr = 0;
getN(v[0], 0, nl, nr, 0);
return;
}
for(int i = 0; i < base[cnt]; i++) {
Node* p1 = v[p[i].x];
Node* p2 = v[p[i].y];
vector<Node*> tv = v;
tv.erase(tv.begin()+p[i].y);
tv.erase(tv.begin()+p[i].x);
Node* p3 = new Node(p1->w+p2->w, p1, p2);
tv.push_back(p3);
getT(cnt-1, tv);
tv.erase(tv.begin()+cnt-2);
delete p3;
p3 = new Node(p1->w+p2->w, p2, p1);
tv.push_back(p3);
getT(cnt-1, tv);
delete p3;
}
}
void deal() {
int cnt = 0;
for(int i = 1; i < 6; i++)
for(int j = 0; j < i; j++) {
p[cnt].x = j;
p[cnt++].y = i;
}
}
int main() {
int t;
cin >> t;
deal();
while(t--) {
getD();
if(n==1) { cout << fixed << showpoint << setprecision(precision) << 0.0 << endl; continue; }
vector<Node*> v;
for(int i = 0; i < n; i++) v.push_back(&node[i]);
getT(n, v);
if(ans < 0) cout << "-1" << endl;
else cout << fixed << showpoint << setprecision(precision) << ans << endl;
}
return 0;
}
注意本题最重要需要解决的便是如何枚举一棵完全二叉树,很明显此时由自下向顶的建树方法是比较容易想到的
假设一个树的结点池,里面存放了当前树的所有叶子结点,从中任意挑选两个结点合并成一个结点(这两个结点此时作为新节点的左右子结点),然后将新结点加入树的结点池
那么现在再对这个结点池进行上一步的建树操作,直到该池子中只剩下一个结点,那么这就是枚举中的一棵树的根节点了
基于上述的想法我们可以很自然的想到,对于给定节点的任意建树的枚举,也是进行同样的操作,不过如果只是一个结点的时候需要进行特殊处理外,其他大于一个结点的结合都是将这个新结点加入池中,重复上一步操作就可以了,这就是对一棵树的枚举的递归定义
当然其中还涵盖了子集合的问题
注意,本题对于一棵确定的二叉树,是可以计算出每个挂坠的确切位置,进而计算出整个天平的宽度
枚举二叉树最直观的方法是沿用回溯法框架,每次选择两个结点组成一棵子树,递归s-1层即可。
注意这个方法虽然很简单,但是仍然会出现重复结点的情况,因此枚举方法最推荐的是自顶向下构造,每次枚举左子树用到哪个子集,则右子树就是使用剩下的子集,第9章会讨论枚举子集的高效算法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)