第三次上机实验解题报告

前言

这次上机实验考试主要考察了同学们对于树、二叉树、堆等树形结构相关的算法的掌握。记得上次考试结束时候谷老师说:“是不是题出得太模板了啊?” 这次果然题目在抽象性上有了不小的提升,不过这也增加了题目的趣味性,毕竟能从一个看似不怎么相关的问题上联想出一种数据结构来还是蛮有成就感的。

7-1 二叉树最长路径

题意简述

给出一个二叉树的先序遍历序列,其中,空子树用数字 − 1 -1 1 表示,求二叉树上的最长链。如果这样的链有多条,则输出所有最长链中最靠右的一条。结点数 n ≤ 1 0 5 n\leq10^5 n105,时间限制 100 ms 100 \text{ms} 100ms。题目要求行尾不能有多余的空格,文末要有一个回车。

关于最靠右的解释

在这里我们形式化地给出“最靠右”的定义:

假设我们有两条长度为 L L L 的树上路径 :

A : [ a 1 , a 2 , ⋯   , a i − 1 , a i , a i + 1 , ⋯   , a L ] A:[a_1, a_2, \cdots, a_{i-1}, a_i, a_{i+1},\cdots, a_L] A:[a1,a2,,ai1,ai,ai+1,,aL]

B : [ a 1 , a 2 , ⋯   , a i − 1 , a i ′ , a i + 1 ′ , ⋯   , a L ′ ] B:[a_1, a_2, \cdots, a_{i-1}, a'_i, a'_{i+1},\cdots, a'_L] B:[a1,a2,,ai1,ai,ai+1,,aL]

其中: a 1 a_1 a1 是树根,位置 i i i 是两个序列第一次出现不同的地方。此时,如果结点 a i a_i ai 是 结点 a i − 1 a_{i-1} ai1 的右子 而 a i ′ a'_i ai 是结点 a i − 1 a_{i-1} ai1 的左子。那么我们称 A A A 路径,比 B B B 路径更靠右

根据更靠右的定义,我们就能够在所有最长路径中,找到最靠右的那个(因为我们能够证明,更靠右是一种偏序关系)。

递归作法

T o    i t e r a t e ,    h u m a n ;    t o    r e c u r s e ,    d i v i n e .    —    L .    P e t e r    D e u t s c h To\;iterate,\;human;\;to\;recurse,\;divine.\;—\;L.\;Peter\;Deutsch Toiterate,human;torecurse,divine.L.PeterDeutsch

我们可以考虑递归地输入整个二叉树,并且递归地从所有路径中找到最长最右的一条。观察到整棵树的最长路径,除去根节点后,其实就是某棵子树的最长路径。在计算一个结点开始的最长路径时,如果我们能够保证,这个结点的左子树和右子树的最长路径都已经求得了,那么我们只需要在这两条最长路径中取更优的一条,然后再接上我们的根节点,就能得到当前树的最长路径。而这个计算过程正是一个对树进行后序遍历的过程。

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 100000 + 6;

int lch[maxn], rch[maxn];
/// lch[i] 表示结点 i 的左子
/// rch[i] 表示结点 i 的右子
/// lch[i] = -1 表示结点 i 没有左子
/// rch[i] = -1 表示结点 i 没有右子

int input() {
	/// input 的功能是读入并建构一棵二叉树,最后返回根节点的编号
	int t; scanf("%d", &t); /// 读入根节点的编号
	if(t != -1) {
		lch[t] = input(); /// 递归输入左子树,并把左子树的根节点接到当前结点的左子上
		rch[t] = input(); /// 递归输入右子树,并把右子树的根节点接到当前结点的右子上
	}
	return t;
}

int h[maxn], bst[maxn];
/// h[i] 表示结点 i 的高度,出于我个人的习惯,我这里定义的高度与教材不同
/// 我规定,叶子结点的高度为 1,空节点的高度为零
/// 对于结点 i: 如果 i 的左子树的高度 要比右子树的高度高,那么 bst[i] = lch[i]
///             否则:bst[i] = rch[i]
/// 这里的 bst 是 best 的缩写,表示最优子节点
/// 特殊地,如果 x 没有子节点 我们规定 bst[x] = 0

void dfs(int x) {
	/// 计算 x 结点的高度 h[x],并找到 x 的最优子节点 bst[x]
	if(x == -1) {
		return;
	}else {
		h[x] = 1; /// 如果 i 没有儿子结点那么 h[x] = 1
		if(lch[x] != -1) { /// 如果 i 有左子
			dfs(lch[x]);   /// 先在左子中递归计算 h 与 bst
			h[x] = max(h[x], h[lch[x]] + 1); /// 这里直接令 h[x] = h[lch[x]] + 1 即可
			bst[x] = lch[x]; /// 将最优子设置为左子
		}
		if(rch[x] != -1) { /// 如果 i 有右子
			dfs(rch[x]);   /// 递归在右子中计算 h 与 bst
			h[x] = max(h[x], h[rch[x]] + 1); /// 与刚才得到的左子的高度 + 1 取较大值
			if(lch[x] == -1 || h[rch[x]] >= h[lch[x]]) { 
				/// 左子不存在 或者右子树高度 大于等于 左子树高度
				/// 将最优子设置为左子
				bst[x] = rch[x];
			}
		}
		/// 上面的代码可能看起来比较繁琐
		/// 这样写是为了考虑左右子树不存在的情况
		/// 如果起初的时候我们用 lch[i] = 0, rch[i] = 0 而不是 lch[i] = -1, rch[i] = -1
		///     表示结点 i 没有左子或右子
		///     代码实现起来会更方便
	}
}

int main() {
	int n; scanf("%d", &n); /// 输入结点总数(我没用上)
	int rt = input();       /// 递归输入一棵二叉树
	dfs(rt); /// 对这棵二叉树进行后序遍历,计算每个节点的 h 和 bst 值
	printf("%d\n", h[rt]-1); /// 根据教材对树高的定义,我们计算的 h 值 是树高 + 1
	while(rt != 0) {
		printf("%d", rt); /// 每次输出当前子树根节点结点的编号 rt 并迭代到 bst[rt] 中继续输出
		rt = bst[rt];     /// 不难证明这样我们输出的结果一定是最优解中最靠右的
		if(rt != 0) putchar(' ');
	}
	putchar('\n');
	return 0;
}

迭代作法

使用了 STL \text{STL} STL 的堆实现消递归。

#include <cstdio>
#include <algorithm>
#include <stack>
using namespace std;

const int maxn = 100000 + 6;
int nxt[maxn], ch[maxn][2];
/// nxt[i] 记录 i 的最后子节点
/// ch[i][0] 表示结点 i 的左子树
/// ch[i][1] 表示结点 i 的右子树

int input() {
    int rt; scanf("%d", &rt);
    stack<pair<int, int> > s;   /// 参数栈
    s.push(make_pair(rt, 1)); /// 0 表示处理左子树,1 表示处理右子树
    s.push(make_pair(rt, 0));
    while(!s.empty()) {
        pair<int, int> tmp = s.top(); s.pop();
        int x = tmp.first;
        int dir = tmp.second;
        scanf("%d", &ch[x][dir]);
        if(ch[x][dir] != -1) {
            s.push(make_pair(ch[x][dir], 1));
            s.push(make_pair(ch[x][dir], 0)); /// 递归读入左右子树
        }else {
            ch[x][dir] = 0; /// 我们用零表示空树
        }
    }
    return rt;
}

int h[maxn]; /// 记录每个节点的高度 + 1

int solve(int rt) {
    stack<pair<int, int> > s;
    s.push(make_pair(rt, -1)); /// -1 表示应该进行数据整合
    s.push(make_pair(rt, 1));
    s.push(make_pair(rt, 0));
    while(!s.empty()) {
        pair<int, int> tmp = s.top(); s.pop();
        int x = tmp.first;
        int dir = tmp.second;
        if(dir == -1) { /// 对结点 x 进行数据整合
            h[x] = max(h[ch[x][0]], h[ch[x][1]]) + 1;
            if(h[ch[x][0]] > h[ch[x][1]]) {
                /// 这里省略了对空结点的判断
                nxt[x] = ch[x][0];
            }else {
                nxt[x] = ch[x][1];
            }
        }else {
            int now = ch[x][dir]; /// 在子结点中递归处理
            if(now != 0) {
                s.push(make_pair(now, -1));
                s.push(make_pair(now, 1));
                s.push(make_pair(now, 0));
            }
        }
    }
    return h[rt] - 1;
} 

int main() {
    int n; scanf("%d", &n);
    int rt = input();
    int ans = solve(rt);
    printf("%d\n", ans);
    while(rt != 0) {
        printf("%d", rt);
        if(nxt[rt] != 0) putchar(' ');
        else putchar('\n');
        rt = nxt[rt];
    }
    return 0;
}

7-2 森林的层次遍历

题意简述

给出一个森林的先序遍历序列,已知每个节点的度(子节点的个数),求整个森林的层次遍历序列。结点数 n ≤ 1 0 5 n\leq10^5 n105,时间限制 100 ms 100 \text{ms} 100ms。题目要求行尾不能有多余的空格,文末要有一个回车。

递归作法

采用与第一题类似的作法递归地从先序遍历序列中建构出一棵树来,得到树之后,我们使用队列对森林进行 B F S BFS BFS ,即可得到森林的层次遍历序列。

#include <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn = 100000 + 6;

char name[maxn][3];    /// name[i] 表示先序遍历中 第 i 个结点的名字
int  degr[maxn];       /// degr[i] 表示先序遍历中 第 i 个结点的度(儿子的个数)
vector<int> son[maxn]; /// son[i] 是结点 i 的所有儿子构成的序列
/// 作为一个懒人,我比较喜欢用 vector 记录每个节点的所有儿子 

int tot = 0;
/// 当我们从左至右地考虑先根序列中的每一个元素时
/// 我们用 tot 表示我们当前已经处理过的结点的个数
/// 这里,“i 已经处理过了” 指的是 i 以及 i 的所有子树已经构建完成

int input() { /// 输入一棵子树,并返回这棵子树的根节点
	++ tot;
	int id = tot;
	/// id 表示当前子树的根结点在先根遍历中的位置为 id
	/// 我们不妨记这个根节点的编号为 id
	for(int i = 1; i <= degr[id]; i ++) {
		/// 由于这个结点的儿子个数 degr[id] 是已知的
		/// 我们递归读入 degr[id] 棵子树,并将它们的根节点一次加入到 son[id] 中
		/// 就得到了完整的结点 id 的儿子序列 son[id] 
		int now = input();
		son[id].push_back(now);
	}
	return id;
}

int main() {
	int n; scanf("%d", &n); /// 输入结点总数
	for(int i = 1; i <= n; i ++) {
		scanf("%s", name[i]);
		/// 按照先根遍历序,输入每个结点的 “名字”(结点可能重名)
		/// 这是我们为每个结点进行编号的原因
	}
	for(int i = 1; i <= n; i ++) {
		scanf("%d", &degr[i]);
		/// 按照先根遍历序,输入每个节点的度
	}
	queue<int> q;
	while(tot != n) {
		/// 由于我们给出的是一个森林的先根遍历序列,因此只执行一次 input 操作可能不能完成整个森林的构建
		/// 我们需要一直循环执行 input() ,指导所有节点都已经被处理过
		/// 因为 tot 表示当前已经被处理过的点的个数,因此所有节点都被处理过的时候一定有 tot == n
		int rt = input();
		q.push(rt); /// 我们把所有根节点依次放入 一个队列 q 中,以便于后面进行 BFS
	}
	while(!q.empty()) {
		int x = q.front(); q.pop();
		/// 当一个结点从队列中弹出的时候,我们就输出这个结点的 “名字”
		/// 并将这个结点的所有后继结点压入队列中
		for(int i = 1; i <= degr[x]; i ++) {
			q.push(son[x][i-1]);
		}
		printf("%s", name[x]);
		if(!q.empty()) putchar(' ');
	}
	putchar('\n');
	return 0;
}

迭代作法

#include <cstdio>
#include <algorithm>
#include <stack>
#include <queue>
#include <vector>
using namespace std;

const int maxn = 100000 + 6;
char name[maxn][2];
int degr[maxn];

vector<int> nxt[maxn]; /// 记录每个节点的所有儿子节点

int tot = 0;
int build() {
    int id = ++ tot;
    stack<int>s;
    for(int i = 1; i <= degr[id]; i ++) { /// 有几个儿子就进栈几次
        s.push(id);
    }
    while(!s.empty()) {
        int x = s.top(); s.pop();
        int son = ++ tot;
        nxt[x].push_back(son);
        for(int i = 1; i <= degr[son]; i ++) {
            s.push(son);
        }
    }
    return id;
}

int main() {
    int n; scanf("%d", &n);
    for(int i = 1; i <= n; i ++) {
        scanf("%s", name[i]);
    }
    for(int i = 1; i <= n; i ++) {
        scanf("%d", &degr[i]);
    }
    queue<int> q;
    while(tot != n) {
        int rt = build();
        q.push(rt);
    }
    while(!q.empty()) {
        int x = q.front(); q.pop();
        printf("%s", name[x]);
        for(int i = 0; i < nxt[x].size(); i ++) {
            q.push(nxt[x][i]);
        }
        if(q.empty()) {
            putchar('\n');
        }else {
            putchar(' ');
        }
    }
    return 0;
}

7-3 纸带切割

题意简述

有一个长度为 L L L 的纸带,现在我们要求你把这条纸带切割成 n n n 段(只在能整数坐标处进行切割),给出这 n n n 段各自的长度,求得到这些段的最小代价,并输出每次切割的代价。我们规定,当我们把一条长度为 K K K 的纸带切割成两部分时,代价为 K K K。整个切割过程中的代价是,每次切割代价之和。当我们进行切割时,题目要求,如果有多种可行的切割方案,每次切割都选择未达最终要求最长纸带切割,若这样的纸带有多条,则任选一条切割。

数据范围:最终纸带总数 n ≤ 1 0 5 n\leq 10^5 n105 , 最终每条纸带的长度 L i ≤ 2 × 1 0 8 L_i\leq 2\times10^8 Li2×108

解题思路

这道题目非常有趣,惊奇地发现每一种最优的切割序列能够与一棵哈夫曼树一一对应。我们考虑分割的逆过程——合并,每次我们从所有纸带中取两条纸带,并将它们连接成一条纸带,我们记我们合并的代价为新生成的纸带的长度。不难证明,初始问题中的“最小切割代价”,与我们现在描述的“最小合并代价”一定是相同的。

用图例解释问题的哈夫曼树性质

例如,样例中: n = 5 , L = { 5 , 6 , 7 , 2 , 4 } n=5, L=\{5, 6, 7, 2, 4\} n=5,L={5,6,7,2,4},我们能够得到这样一棵哈夫曼树

24
13
11
6
7
5
6
2
4

从分割角度看

简单地说,就是这样一个过程:

{ 24 } ⇒ { 11 , 13 ‾ } ⇒ { 6 , 7 ‾ , 11 } ⇒ { 5 , 6 ‾ , 6 , 7 } ⇒ { 2 , 4 ‾ , 5 , 6 , 7 } \{\bold{24}\}\Rightarrow\{\underline{11, \bold{13}}\}\Rightarrow\{\underline{6, 7}, \bold{11}\}\Rightarrow\{\underline{5, 6}, \bold{6}, 7\}\Rightarrow\{\underline{2, 4}, 5, 6, 7\} {24}{11,13}{6,7,11}{5,6,6,7}{2,4,5,6,7}

粗体字表示当前被切割的纸带的长度,下划线表示上次切割生成的纸带的长度。

从合并的角度看

起初我们有五条纸带,纸带长度集合为 { 2 , 4 , 5 , 6 , 7 } \{2, 4, 5, 6, 7\} {2,4,5,6,7},我们可以按照这样的过程进行合并:

{ 2 , 4 , 5 , 6 , 7 } ⇒ { 5 , 6 , 6 , 7 } ⇒ { 6 , 7 , 11 } ⇒ { 11 , 13 } ⇒ { 24 } \{\bold{2, 4}, 5, 6, 7\}\Rightarrow\{\bold{5, 6}, 6, 7\}\Rightarrow\{\bold{6, 7}, 11\}\Rightarrow\{\bold{11, 13}\}\Rightarrow\{24\} {2,4,5,6,7}{5,6,6,7}{6,7,11}{11,13}{24}

从哈夫曼树的角度来看

我们切割的总代价是每个內点(非叶子节点)的权值的和,即:

24 + 13 + 11 + 6 24+13+11+6 24+13+11+6

而每个內点的权值,是其子树中所有叶子结点的和:

= ( 2 + 4 + 5 + 6 + 7 ) + ( 6 + 7 ) + ( 2 + 4 + 5 ) + ( 2 + 4 ) =(2+4+5+6+7)+(6+7)+(2+4+5)+(2+4) =(2+4+5+6+7)+(6+7)+(2+4+5)+(2+4)

也就是:

= 2 × 3 + 4 × 3 + 5 × 2 + 6 × 2 + 7 × 2 = ∑ i = 1 n L i ⋅ D e p t h i =2\times3+4\times3+5\times2+6\times2+7\times2\\=\sum_{i=1}^n L_i\cdot Depth_i =2×3+4×3+5×2+6×2+7×2=i=1nLiDepthi

其中 L i L_i Li 表示所有分割结束后,第 i 条的长度, D e p t h i Depth_i Depthi 表示第 i i i 条纸带在分割树上的深度。

因此,我们能够证明,当我们想要最小化分割总代价时, 其实就是在最小化带权外通路长度。而带权外通路长度的最小化可以使用哈夫曼树解决,因此,具体实现的过程类似于纸带的合并(而不是分割)。

代码实现

#include <cstdio>
#include <queue>
#include <vector>
#include <vector>
#include <algorithm>
using namespace std;

typedef long long ll;

struct cmp {
	/// 比较结构体:用来指导优先队列形成一个小根堆
	/// 否则优先队列默认实现大根堆
	bool operator()(long long A, long long B) {
		return A > B;
	}
};

priority_queue<long long, vector<long long>, cmp> pq;

long long ans;
vector<long long> ary;

int main() {
	int n; scanf("%d", &n);
	for(int i = 1; i <= n; i ++) {
		ll x; scanf("%lld", &x);
		pq.push(x);
		/// 输入所有纸带的长度并压入堆(优先队列)中
	}
	while(pq.size() > 1) {
		/// 每次选择长度最小的两条纸带进行合并
		long long L = pq.top(); pq.pop();
		long long R = pq.top(); pq.pop();
		pq.push(L + R);
		ary.push_back(L + R); /// 将当前的合并代价储存起来
		ans += L + R; /// 统计合并的总花费
	}
	printf("%lld\n", ans);
	for(int i = (int)ary.size() - 1; i >= 0; i --) {
		/// 反向输出每次操作的代价
		/// 由于体面所说的分割实际上是合并的逆过程
		/// 因此需要反向输出
		printf("%lld", ary[i]);
		if(i != 0) putchar(' ');
		else putchar('\n');
	}
	return 0;
}

关于反向输出的正确性

为什么我们排序后得到的操作序列一定符合体面条件的约束——“每次切割都选择未达最终要求的最长纸带切割,若这样的纸带有多条,则任选一条切割。”

这里有哈夫曼树的一个性质:任意一个后生成的结点,它的权值一定大于等于在它之前生成的全部结点。具体证明可以采用反证法+抽屉原理。某个某时刻,根据哈夫曼树算法,某个结点由合并生成,权值为 w 1 w_1 w1;下一时刻,又根据哈夫曼树算法,另一个节点由合并生成,权值为 w 2 w_2 w2,假设 w 2 < w 1 w_2<w_1 w2<w1

倘若 w 2 w_2 w2 w 1 w_1 w1 的另一个结点合并得到,因为每个节点的权值均非负,显然有 w 2 ≥ w 1 w_2 \geq w_1 w2w1,与假设不成立,否则:

我们记 w 1 w_1 w1 w 1 ′ w_1' w1 w 1 ′ ′ w_1'' w1 合并得到,即 w 1 = w 1 ′ + w 1 ′ ′ w_1=w_1'+w_1'' w1=w1+w1;记 w 2 w_2 w2 w 2 ′ w_{2}' w2 w 2 ′ ′ w_2'' w2 合并得到,即 w 2 = w 2 ′ + w 2 ′ ′ w_2=w_2'+w_2'' w2=w2+w2。不妨假设 w 1 ′ ≤ w 1 ′ ′ , w 2 ′ ≤ w 2 ′ ′ w_1'\leq w_1'',w_2'\leq w_2'' w1w1,w2w2。则有:

w 1 ′ ′ ≥ w 1 2 , w 2 ′ ≤ w 2 2 w_1''\geq\frac{w_1}{2}, w_2'\leq \frac{w_2}{2} w12w1,w22w2

即:

w 2 ′ ≤ w 2 2 < w 1 2 ≤ w 1 ′ ′ w_2'\leq \frac{w_2}{2}<\frac{w_1}{2}\leq w_1'' w22w2<2w1w1

因此我们能说明 w 1 w_1 w1 的生成过程一定违背了哈夫曼树算法,因为在生成 w 1 w_1 w1 时,被合并的两个结点的权值应该是 w 1 ′ w_1' w1 w 2 ′ w_2' w2 而不是 w 1 ′ w_1' w1 w 1 ′ ′ w_1'' w1 (省略了一些繁琐的证明细节)。

代码实现

#include <cstdio>
#include <queue>
#include <vector>
#include <vector>
#include <algorithm>
using namespace std;

typedef long long ll;

struct cmp { /// 比较结构体
	bool operator()(long long A, long long B) { /// 引导优先队列形成小根堆
		return A > B;
	}
};

priority_queue<long long, vector<long long>, cmp> pq;

long long ans; /// 用于记录所有 分割(合并)的总代价
vector<long long> ary; /// 用于记录每次 分割(合并)产生的代价

int main() {
	int n; scanf("%d", &n);
	for(int i = 1; i <= n; i ++) {
		ll x; scanf("%lld", &x); /// 输入每一段纸带的长度
		pq.push(x);
	}
	while(pq.size() > 1) {
		long long L = pq.top(); pq.pop(); /// 每次选择最短的两条纸带合并
		long long R = pq.top(); pq.pop();
		pq.push(L + R);
		ary.push_back(L + R); /// 记录这次合并的代价
		ans += L + R;         /// 记录合并的总代价
	}
	printf("%lld\n", ans); /// 输出合并的总代价
	for(int i = (int)ary.size() - 1; i >= 0; i --) { /// 输出每次合并的代价
		printf("%lld", ary[i]);
		if(i != 0) putchar(' ');
		else putchar('\n');
	}
	return 0;
}

7-4 序列乘积

题意简述

给出两个长度为 n n n 的递增序列 A A A B B B,要求求出 { A [ i ] × B [ j ] ∣ i = 1 ⋯ n , j = 1 ⋯ n } \{A[i]\times B[j]|i=1\cdots n,j=1\cdots n\} {A[i]×B[j]i=1n,j=1n} n 2 n^2 n2 个整数中,最小的 n n n 个整数。

n ≤ 1 0 5 , A i , B i ≤ 4 × 1 0 4 n \leq 10^5, A_i,B_i\leq 4\times 10^4 n105,Ai,Bi4×104,时间限制 100 ms 100\text {ms} 100ms,内存限制 5 MB 5\text{MB} 5MB

解题思路

我们以样例为例:

A = [ 1 , 3 , 5 , 7 , 9 ] , B = [ 2 , 4 , 6 , 8 , 10 ] A=[1, 3, 5, 7, 9], B=[2, 4, 6, 8, 10] A=[1,3,5,7,9],B=[2,4,6,8,10]

我们把所有的 A i × B j A_i\times B_j Ai×Bj 写到一个数表中,其中第 i i i 行 第 j j j 列表示 A i × B j A_i\times B_j Ai×Bj。不难发现,这样构建的数表的每一行都是单调递增的,因此不难说明,整个数表的最小值一定在每行的最小值(也就是下图中画矩形的结点)中产生。

heap
2 i=1, j=1
6 i=2, j=1
10 i=3, j=1
14 i=4, j=1
18 i=5, j=1
min=2
4
6
8
10
12
18
24
30
20
30
40
50
28
42
56
70
36
54
72
90

此时,我们不妨把当前数表中最小值删去,得到一个新的数表:

heap
4 i=1, j=2
6 i=2, j=1
10 i=3, j=1
14 i=4, j=1
18 i=5, j=1
min=4
6
8
10
12
18
24
30
20
30
40
50
28
42
56
70
36
54
72
90

这个时候,我们发现,整个数表的最小值,一定还在每行的最小值中产生。此时,数表中剩余数据的最小值是 4 4 4

由于我们的内存中存不下一个 n × n n\times n n×n 的数表,我们不妨考虑,用一个堆来记录上图中每行的最小值的信息(包括行号、列号以及数值),每当我们从堆中弹出一个元素的时候,我们就将这个元素的后继元素推入堆中,根据我们记录的行号和列号,我们能很轻松地计算出一个元素的后继元素。这里的后继元素指的就是同一行中的下一个元素。例如 ( i , j , A i × B j ) (i, j, A_i\times B_j) (i,j,Ai×Bj) 的后继元素就是 ( i , j + 1 , A i × B j + 1 ) (i, j+1, A_i\times B_{j+1}) (i,j+1,Ai×Bj+1)

代码实现

#include <cstdio>
#include <vector>
#include <queue>
using namespace std;

const int maxn = 100000 + 6;
int A[maxn], B[maxn];

struct node { /// 表示一个三元组 (l, r, A[l]*B[r])
	int l, r;
	int v;
};

struct cmp { /// 比较结构体
	bool operator()(node A, node B) { /// 指导优先队列生成一个小根堆
		return A.v > B.v;
	}
};

priority_queue<node, vector<node>, cmp> pq; /// 一个小根堆

int main() {
	int n; scanf("%d", &n);
	for(int i = 1; i <= n; i ++) {
		scanf("%d", &A[i]);
	}
	for(int i = 1; i <= n; i ++) {
		scanf("%d", &B[i]);
	}
	for(int i = 1; i <= n; i ++) {
		pq.push((node){i, 1, A[i]*B[1]});
		/// 起初,我们把每一行的最小元素插入堆中
	}
	for(int i = 1; i <= n; i ++) {
		node tmp = pq.top(); pq.pop();
		pq.push((node){tmp.l, tmp.r+1, A[tmp.l]*B[tmp.r+1]});
		printf("%d", tmp.v);
		/// 每次我们从堆中弹出一个最小的元素并且把这个元素的后继元素推入堆中
		/// 由于我们只需要求最小的 n 个元素,所以我们不需要考虑某一行被弹空的情况
		if(i != n) putchar(' ');
		else putchar('\n');
	}
	return 0;
}

后记

希望明天的上机实验不会很难。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值