分支限界
整体结构
本质上感觉还是遍历解树+剪枝,但是配合优先队列使用以后可以更好的找到最优解。
例题
P8011 ⾛迷宫
对于迷宫问题,某一节点的关联节点指的是它四个方向上相邻的节点。
要利用flag数组确保不会重复访问。
void bfs(){ //1、初始化队列queue ,将第一个节点放入队列 t++; q[t].x = 1; q[t].y = 1; q[t].step = 0; flag[1][1] = true; //2.循环遍历队列 while(h <= t){ //队列不空 // 3.取出队头,存入cur Node cur = q[h]; h++; // 4. 利用产生式规则,拓展cur的关联节点入队 for(int i = 0; i < 4; i++) { //四个方向拓展 int nx = cur.x + z[0][i]; int ny = cur.y + z[1][i]; if(nx < 1 || nx > n || ny < 1 || ny > m) continue;//越界 if(a[nx][ny] == 1) continue; //有墙 if(flag[nx][ny]) continue; //走过 // 如果找到答案,结束搜索 if(nx == n && ny == m){ ans = cur.step + 1;//拓展出的节点到了终点,所以要把最后一步加上 return ; } //既没跳过也没到终点,那就先存起来 flag[nx][ny] = true;//拓展的过程中就已经用过 t++;//存起来只是为了继续拓展 q[t].x = nx; q[t].y = ny; q[t].step = cur.step + 1; } } }
P8012 01背包
核心在于选和不选两个分支的剪枝,总体框架依然是取队头-判断剪枝-拓展-判断剪枝-入队。
void bfs(){ //1、初始化队列queue ,将第0个物品放入队列 t++; q[t].i = 0; //第0个物品 q[t].cp = 0; q[t].cv = 0; bestp = 0; //2.循环遍历队列 while(h <= t){ //队列不空 // 3.取出队头,存入cur Node cur = q[h]; h++; int n_i = cur.i+1; if(n_i > n) continue;//物品用完了 // 4. 利用产生式规则,拓展cur的关联节点入队 // 选择下一个物品,并入队 if(cur.cv + items[n_i].volume <= V){ //装不下则剪掉选择该物品的分支 t++; q[t].i = n_i; q[t].cp = cur.cp + items[n_i].price; q[t].cv = cur.cv + items[n_i].volume; bestp = max(bestp,q[t].cp); } // 计算上界 double b = bound(n_i + 1, cur.cv, cur.cp); // 不选择下一个物品,且上界大于当前最佳解时,才将节点入队 if (b > bestp) { t++; q[t].i = n_i; q[t].cp = cur.cp; q[t].cv = cur.cv; } } }
把手动固定队列换成优先队列的版本。由于优先队列会把价值密度更高的物品推到前面,所以程序会优先沿着局部最优的路径往下走,更便于找到最优路径。
// 更快找到最优解:先探索最有希望的节点(即优先级最高的节点) // 更有效的剪枝:优先处理那些最有可能导致最优解的节点。 double bound(int i, int cv, int cp) { double maxp = cp; int totv = cv; // 使用贪心策略继续添加物品 while (i <= n && totv + items[i].volume <= V) { totv += items[i].volume; maxp += items[i].price; i++; } // 如果还有剩余空间,则按比例取最后一个物品的价值 if (i <= n) { maxp += (V - totv) * (items[i].density); } return maxp; } void bfs(){ //1、初始化队列queue ,将第0个物品放入队列 Node cur; priority_queue<Node> p_q; cur.i = 0; //第0个物品 cur.cp = 0; cur.cv = 0; cur.ub = bound(cur.i+1, cur.cv, cur.cp);//计算上界值 p_q.push(cur); //插入优先队列 //2.循环遍历队列 while(!p_q.empty()){ //队列不空 // 3.取出队头,存入cur cur = p_q.top(); p_q.pop(); int n_i = cur.i+1; if(n_i > n) continue; // 4. 利用产生式规则,拓展cur的关联节点入队 // 选择下一个物品,并入队 if(cur.cv + items[n_i].volume <= V){ // 左剪枝 Node t; t.i = n_i; t.cp = cur.cp + items[n_i].price; t.cv = cur.cv + items[n_i].volume; t.ub = bound(cur.i+1, cur.cv, cur.cp);//计算上界值 bestp = max(bestp,t.cp); p_q.push(t); //插入优先队列 } // 不选择下一个物品,并入队 // 计算上界 Node t2; t2.i = n_i; t2.cp = cur.cp; t2.cv = cur.cv; t2.ub = bound(cur.i+1, cur.cv, cur.cp);//计算上界值 if (t2.ub > bestp) { p_q.push(t2); //插入优先队列 } } }
P8013 任务分配
总体框架不变,确定下界的自定义函数变了,多了一个枚举任务的逻辑,多了一个任务分配状态的记录,其他就比较常规啦。
void bound(Node &e){//当前还有可能得到的最小代价 int minsum = 0; for(int i = e.i + 1; i <= n; i++){ // 枚举人 int min_v = 2e9; // 贪心 for(int j = 1; j <= n; j++){ // 枚举任务 if(e.used[j] == 0 && a[i][j] <= min_v) min_v = a[i][j]; } minsum += min_v; } e.lb = e.cost + minsum; } void bfs(){ // 定义一个优先队列 priority_queue<Node> p_q; Node cur,next; cur.i = 0; //根节点 cur.cost =0; cur.used.resize(n+10); cur.ve.resize(n+10); bound(cur); p_q.push(cur); //根节点入队 while(!p_q.empty()){ // 队列不为空 cur = p_q.top(); p_q.pop(); for(int j = 1; j <= n; j++){ //枚举任务 if(cur.used[j] == 1) continue; //任务被分配过 next.i = cur.i + 1; next.used = cur.used; next.ve = cur.ve; next.used[j] = 1; next.ve[next.i] = j; //第i个人,被分配了第j个任务 next.cost = cur.cost + a[next.i][j]; bound(next);//计算下界 if(next.lb < ans){//剪枝 if(next.i == n){ //更新最优解 if(next.cost < ans){ ans = next.cost; } }else{ //入队 p_q.push(next); } } } } }