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章会讨论枚举子集的高效算法

posted @   banyanrong  阅读(43)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示