HDU 2489 Minimal Ratio Tree

传送门

dfs/状压(没试,参考这个)+ mst。
n个点的无向图、完全图,让你在其中找一个m个点的树,满足这个树的边权点权之比在所有m个点的树中最小。给出这m个点,若答案不唯一给出字典序最小的点列表。

可以想到,当这m个点确定了以后,求这m个点的mst就行了,所以,问题在于选取哪些m个点。

主要思想就是dfsn个中选择m个( C n m C^m_n Cnm ),每一个点都有两种选择,O(2^n)复杂度,直到运行到最后一个点,这样形成了一个长度为n的01序列,若其中选了m个,那么就运行prim(优先队列版本)。

这种【nm】的dfs写法记住就行了(其中有两个剪枝,第一个剪枝是基本的),可以顺便再回顾一下这道题dfs写法。
而且这个dfs写法有讲究的是从1开始先选再不选,这样可以保证如果存在多个相等答案,字典序最小的那个一定先被赋给ans

#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;

const int INF = 1e9;
const int MAXN = 15 + 1;

int N, M;
int weight[MAXN];          // 点权
int g[MAXN][MAXN];         // 边权,完全图
bool select[MAXN];         // 当前选择的 M 个点
bool ans[MAXN];            // 当前最优的 M 个点
double opt;

struct Node
{
	int n, d;
	bool operator<(const Node& n0) const
	{
		return d > n0.d;
	}
};

void init()
{
	opt = INF;
}

double prim()                        // 求部分点的最小生成树,反正题目是完全图,总能找到边
{
	int node_sum = 0;
	int edge_sum = 0;

	int d[MAXN];
	bool vis[MAXN];
	priority_queue<Node> q;

	bool first = true;
	for (int i = 1; i <= N; i++)     // 初始化
	{
		if (select[i])
		{
			node_sum += weight[i];
			d[i] = INF;
			vis[i] = false;
			if (first)
			{
				d[i] = 0;
				q.push(Node{ i,d[i] });
				first = false;
			}
		}
	}

	for (; !q.empty();)
	{
		Node u = q.top();
		q.pop();
		if (vis[u.n]) continue;

		vis[u.n] = true;
		edge_sum += u.d;

		for (int j = 1; j <= N; j++)
		{
			if (select[j] && !vis[j] && g[u.n][j] < d[j])
			{
				d[j] = g[u.n][j];
				q.push(Node{ j,d[j] });
			}
		}
	}
	return (double)edge_sum / (double)node_sum;
}

void dfs(int v, int num)             // 当前已选num个点,判断点v选不选
{                                    // 从n个里面选m个,每一个选与不选,2^n的复杂度,但这里n就15
	if (num > M) return;             // 第一个剪枝
	if (num + N - v + 1 < M) return; // 第二个剪枝(若之后的都选也达不到m),也可以不加,加上之后下面的 num==M 就可以去掉了  
	if (v == N + 1)                  // 递归出口
	{
		if (num == M)
		{
			double t = prim();
			if (t < opt)
			{
				opt = t;
				memcpy(ans, select, sizeof ans);
			}
		}
		return;
	}

	select[v] = true;                // 这样的展开次序可以保证字典序最小
	dfs(v + 1, num + 1);
	select[v] = false;
	dfs(v + 1, num);
}

int main()
{
	for (; ~scanf("%d%d", &N, &M);)
	{
		if (N == 0 && M == 0) break;
		init();
		for (int i = 1; i <= N; i++)
			scanf("%d", &weight[i]);
		for (int i = 1; i <= N; i++)
			for (int j = 1; j <= N; j++)
				scanf("%d", &g[i][j]);

		dfs(1, 0);
		bool first = true;
		for (int i = 1; i <= N; i++)
		{
			if (ans[i])
			{
				if (!first) printf(" ");
				printf("%d", i);
				first = false;
			}
		}
		printf("\n");
	}

	return 0;
}

(19年5月25日)上述的代码是走到num > M处才剪枝,然后最后统一到v == N+1处得出答案。
下面的代码简单改了一下,还是像二叉树那样展开,不同的是,走到num == M处就立即运算结果并剪枝;而且,不再用一个长度为n的01序列来表示,而是用一个vector来动态地增减元素。

可以想到,每个点都有两个分支(第i个点最多有2^(i-1)次调用),每个dfs过程执行完毕之后select都是空的(从底向上,按照数学归纳法考虑)。

这道题dfs写法其实有共通之处。

#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;

const int INF = 1e9;
const int MAXN = 15 + 1;

int N, M;
int weight[MAXN];          // 点权
int g[MAXN][MAXN];         // 边权,完全图
vector<int> select;
vector<int> ans;
double opt;

struct Node
{
	int n, d;
	bool operator<(const Node& n0) const
	{
		return d > n0.d;
	}
};

void init()
{
	opt = INF;
	//select.clear();                // 没必要。。dfs自动收拾干净了
	//ans.clear();
}

double prim()                        // 求部分点的最小生成树,反正题目是完全图,总能找到边
{
	int node_sum = 0;
	int edge_sum = 0;

	int d[MAXN];
	bool vis[MAXN];
	priority_queue<Node> q;

	bool first = true;
	for (int i = 0; i < M; i++)     // 初始化
	{
		int i0 = select[i];
		node_sum += weight[i0];
		d[i0] = INF;
		vis[i0] = false;
		if (first)
		{
			d[i0] = 0;
			q.push(Node{ i0,d[i0] });
			first = false;
		}
	}

	for (; !q.empty();)
	{
		Node u = q.top();
		q.pop();
		if (vis[u.n]) continue;

		vis[u.n] = true;
		edge_sum += u.d;

		for (int j = 0; j < M; j++)
		{
			int j0 = select[j];
			if (!vis[j0] && g[u.n][j0] < d[j0])
			{
				d[j0] = g[u.n][j0];
				q.push(Node{ j0,d[j0] });
			}
		}
	}
	return (double)edge_sum / (double)node_sum;
}

void dfs(int v)
{
	int num = select.size();
	if (num == M)                        // 在这里v绝对不会>N+1
	{
		double t = prim();
		if (t < opt)
		{
			opt = t;
			ans = select;
		}
		return;
	}
	if (num + N - v + 1 < M) return;     // 剪枝,而且其作用已经蕴含了下句(递归出口),下句可以删了,因为到这里必然有 num<M  
	if (v == N + 1) return;

	select.push_back(v);
	dfs(v + 1);
	select.pop_back();
	dfs(v + 1);
}

int main()
{
	for (; ~scanf("%d%d", &N, &M);)
	{
		if (N == 0 && M == 0) break;
		init();
		for (int i = 1; i <= N; i++)
			scanf("%d", &weight[i]);
		for (int i = 1; i <= N; i++)
			for (int j = 1; j <= N; j++)
				scanf("%d", &g[i][j]);

		dfs(1);
		bool first = true;
		for (int i = 0; i < M; i++)
		{
			if (!first) printf(" ");
			printf("%d", ans[i]);
			first = false;
		}
		printf("\n");
	}

	return 0;
}
西北工业大学NOJC程序设计习题答案(非本人制作,侵删) 1.“1“的传奇 2.A+B 3.A+BⅡ 4.AB 5.ACKERMAN 6.Arithmetic Progressions 7.Bee 8.Checksum algorithm 9.Coin Test 10.Dexter need help 11.Double 12.Easy problem 13.Favorite number 14.Graveyard 15.Hailstone 16.Hanoi Ⅱ 17.Houseboat 18.Music Composer 19.Redistribute wealth 20.Road trip 21.Scoring 22.Specialized Numbers 23.Sticks 24.Sum of Consecutive 25.Symmetric Sort 26.The Clock 27.The Ratio of gainers to losers 28.VOL大学乒乓球比赛 29.毕业设计论文打印 30.边沿与内芯的差 31.不会吧,又是A+B 32.不屈的小蜗 33.操场训练 34.插入链表节点 35.插入排序 36.插入字符 37.成绩表计算 38.成绩转换 39.租车费 40.除法 41.创建与遍历职工链表 42.大数乘法 43.大数除法 44.大数加法 45.单词频次 46.迭代求根 47.多项式的猜想 48.二分查找 49.二分求根 50.发工资的日子 51.方差 52.分离单词 53.分数拆分 54.分数化小数 55.分数加减法 56.复数 57.高低交换 58.公园喷水器 59.韩信点兵 60.行程编码压缩算法 61.合并字符串 62.猴子分桃 63.火车站 64.获取指定二进制位 65.积分计算 66.级数和 67.计算A+B 68.计算PI 69.计算π 70.计算成绩 71.计算完全数 72.检测位长宽 73.检查像文件格式 74.奖金发放 75.阶乘合计 76.解不等式 77.精确幂乘 78.恐怖水母 79.快速排序 80.粒子裂变 81.链表动态增长或缩短 82.链表节点删除 83.两个整数之间所有的素数 84.路痴 85.冒泡排序 86.你会存钱吗 87.逆序整数 88.排列 89.排列分析 90.平均值函数 91.奇特的分数数列 92.求建筑高度 93.区间内素数 94.三点顺序 95.山迪的麻烦 96.删除字符 97.是该年的第几天 98.是该年的第几天? 99.数据加密 100.搜索字符 101.所有素数 102.探索合数世纪 103.特殊要求的字符串 104.特殊整数 105.完全数 106.王的对抗 107.危险的组合 108.文件比较 109.文章统计 110.五猴分桃 111.小型数据库 112.幸运儿 113.幸运数字”7“ 114.选择排序 115.寻找规律 116.循环移位 117.延伸的卡片 118.羊羊聚会 119.一维数组”赋值“ 120.一维数组”加法“ 121.勇闯天涯 122.右上角 123.右下角 124.圆及圆球等的相关计算 125.圆及圆球等相关计算 126.程序员添加行号 127.找数字 128.找幸运数 129.找最大数 130.整数位数 131.重组字符串 132.子序列的和 133.子字符串替换 134.自然数立方的乐趣 135.字符串比较 136.字符串复制 137.字符串加密编码 138.字符串逆序 139.字符串排序 140.字符串替换 141.字符串左中右 142.组合数 143.最次方数 144.最大乘积 145.最大整数 146.最小整数 147.最长回文子串 148.左上角 149.左下角
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值