7-1 二叉树最长路径 (100 分)
给定一棵二叉树T,求T中的最长路径的长度,并输出此路径上各结点的值。若有多条最长路径,输出最右侧的那条。
输入格式:
第1行,1个整数n,表示二叉树有n个结点, 1≤n≤100000.
第2行,2n+1个整数,用空格分隔,表示T的扩展先根序列, -1表示空指针,结点用编号1到n表示。
输出格式:
第1行,1个整数length,length表示T中的最长路径的长度。
第2行,length+1个整数,用空格分隔,表示最右侧的最长路径。
输入样例:
在这里给出一组输入。例如:
5
1 2 -1 -1 3 4 -1 -1 5 -1 -1
输出样例:
在这里给出相应的输出。例如:
2
1 3 5
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 100 ms
内存限制 5 MB
解法一:
思路:
解决本题有三个关键问题:
1.如何通过拓展先根序列建树。
2.如何在遍历的同时保存最长路径。
3.最关键的一点:如何兼顾时间和空间复杂度。
对于第一个问题,很容易想到递归。
对于第二个问题,有三种做法:
首先,用先序遍历是一定的,可以先遍历右子树再遍历左子树,这样比较长度时只需要用">",从左子树开始遍历只需要用">="就行。
1.用一个临时数组保存当前路径,通过比较和拷贝来不断维护一个最右最长的路径数组。
2.将所有路径存入一个容器当中,最后取长度最大最右者(依照存入顺序保证)输出。
3.用一个临时结点保存当前访问的有效结点,通过比较和拷贝不断维护一个最右最长路径的叶子结点地址,最后通过叶子结点往回访问(需要parent数据域)。
对于第三个问题,一开始我使用了STL,倒数第二个点直接内存爆掉,后来消掉STL倒数第二个点又超时,最后消掉递归来遍历,倒数第二个点还是超时。(我甚至连读入优化都试过了,最多只能优化10ms左右......)
按理说倒数第二个点数据量不会大于倒数第一个点,我倒数第一个点20ms内通过了。于是考虑是我使用了上述第一种方法来保存最长路径,倒数第二个点或许正好需要大量拷贝操作,此时开始考虑用第三种方法,即:增加一个parent域,这样只需要保存最长路径最后一个叶子结点即可,最后再读取该路径所有数据以输出,然而当我增加了parent域时,最后两个点都直接爆内存了......痛苦。 体会到了空间与时间不可兼得的煎熬。
但是思考片刻我意识到:只需要在建树时将parent域指向父亲结点,再同时保存最右最长路径的叶子结点的地址,建树完毕直接循地址输出其所在路径即可,也就是说其实根本不用遍历,也不用left和right域,只需要parent域!这种方法时间空间上都满足了要求。
遗留问题:
转为这种方法后倒数第二个点通过了,然而最后一个点的时间从20ms左右变成了50ms左右。保留疑问。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
typedef struct Node Node;
int now=0,*source=NULL,cupdepth=0,depth=0;
Node* ans = NULL;
vector<int> out;
struct Node {
int val;
Node* parent;//只保留父亲域
Node(int val):val(val),parent(NULL){}
};
Node* buildtree(int* source, Node* parent) {
if (source[now] == -1) {
now++;
return NULL;
}
Node* root = new Node(source[now++]);
cupdepth++;//向下建立左右子树,深度+1
if (cupdepth >= depth) {
depth = cupdepth;
ans = root;
}
buildtree(source, root);//建立左子树
buildtree(source, root);//建立右子树
cupdepth--;//左右子树建立完毕,向上回溯,深度-1
root->parent = parent;
return root;
}
void getans() {//通过最右最长路径的叶子结点向上获取整个路径
Node* cup = ans;
while (cup != NULL) {
out.push_back(cup->val);
cup = cup->parent;
}
}
int main(){
int n = 0;
scanf("%d", &n);
source = new int[2 * n + 1];
for (int i = 0; i < 2 * n + 1; i++) {
scanf("%d", &source[i]);
}
Node* root = buildtree(source,(Node*)NULL);
getans();
printf("%d\n", depth-1);
for (int i = 0; i < out.size(); i++) {
printf("%d", out[out.size() - 1 - i]);
if (i < out.size() - 1) {
printf(" ");
}
}
}
法一(未满分):
即之前说的倒数第二个点被卡时间的解法。
留作启示和警告:解题时首先考虑满足题目需要即可,无需拘泥于传统。
代码实现:
#include <iostream>
#include <vector>
#include <deque>
#include <stdio.h>
#include <stdlib.h>
#include <list>
using namespace std;
int *cupp;
int * ans;
int pcupp;
int pans;
int Gray = 1;
int White = 0;
int now=0;
struct Node {
int val;
struct Node* left, *right;
};
int read() {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch>'9') { if (ch == '-')f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
return x * f;
}
typedef struct Node Node;
Node* buildtree(list<int>& source) {
if (source.front() == -1) {
source.pop_front();
return NULL;
}
Node* root = new Node;
root->val = source.front();
source.pop_front();
root->left = buildtree(source);
root->right = buildtree(source);
return root;
}
Node* buildtree(int *source) {
if (source[now] == -1) {
now++;
return NULL;
}
Node* root = new Node;
root->val = source[now++];
root->left = buildtree(source);
root->right = buildtree(source);
return root;
}
void mycopy() {
for (int i = 0; i < pcupp; i++) {
ans[i] = cupp[i];
}
pans = pcupp;
}
/*inline void travel(Node* root,int* lj) {
if (root == NULL) {
if (pcupp > pans) {
mycopy();
//pans = pcupp;
//copy(cupp,cupp + pcupp, ans);
}
return;
}
lj[pcupp++]=root->val;
travel(root->right, lj);
travel(root->left, lj);
pcupp--;
}*/
void travel(Node* root, int* lj) {
if (root == NULL) {
return;
}
vector<pair<Node*,int>> stk;
stk.push_back(make_pair(root,White));
pair<Node*, int> now;
while (!stk.empty()) {
now = stk.back();
stk.pop_back();
if (now.first == NULL) {
if (pcupp > pans) {
mycopy();
}
continue;
}
if (now.second != Gray) {
cupp[pcupp++] = now.first->val;
stk.push_back(make_pair(now.first, Gray));
stk.push_back(make_pair(now.first->left, White));
stk.push_back(make_pair(now.first->right, White));
}
else {
pcupp--;
}
}
}
int main() {
int n = 0;
vector<Node*> stk;
Node* now=NULL;
//deque<int> source;
n=read();
n = 2 * n + 1;
int cup = 0;
int* source = new int[2 * n + 1];
cupp = new int[2 * n + 1];
ans= new int[2 * n + 1];
for (int i = 0; i < n; i++) {
source[i]=read();
}
Node* root = buildtree(source);
travel(root, cupp);
/*
stk.push_back(root);
while (!stk.empty()) {
now = stk.back();
if (now == NULL) {
push_
}
stk.push_back(root->left);
stk.push_back(root->right);
}
*/
/*for (int i = 0; i < ans.size(); i++) {
if (ans[i].size() > ans[max].size()) {
max = i;
}
}*/
printf("%d\n", pans - 1);
for (int i = 0; i < pans; i++) {
printf("%d",ans[i]);
if (i < pans - 1) {
printf(" ");
}
}
}
7-2 森林的层次遍历 (100 分)
给定一个森林F,求F的层次遍历序列。森林由其先根序列及序列中每个结点的度给出。
输入格式:
第1行,1个整数n,表示森林的结点个数, 1≤n≤100000.
第2行,n个字符,用空格分隔,表示森林F的先根序列。字符为大小写字母及数字。
第3行,n个整数,用空格分隔,表示森林F的先根序列中每个结点对应的度。
输出格式:
1行,n个字符,用空格分隔,表示森林F的层次遍历序列。
输入样例:
在这里给出一组输入。例如:
14
A B C D E F G H I J K L M N
4 0 3 0 0 0 0 2 2 0 0 0 1 0
输出样例:
在这里给出相应的输出。例如:
A M B C G H N D E F I L J K
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 100 ms
内存限制 5 MB
解法一:
思路:
解决本题关键有两点:
1.如何根据森林的先根序列以及其中每个结点的度建立森林。
2.如何对该种形式的森林进行层次遍历。
对于第一个问题,观察序列对应的建树操作这种逐级深入的特征,我想到了DFS,而存储的话,选择左孩子右兄弟表示法,这样不必担心每个结点的孩子数量。
DFS思路:若当前要建立的结点度为0,则直接建立当前结点并接到最后一个兄弟后面,或者接到父亲下面;若度大于0,则建立当前结点后,以当前结点为父亲,循环进入下一层递归,将当前结点的孩子结点创建完毕。
对于第二个问题,使用队列进行辅助即可,先将根结点入队,每次迭代出队队首结点,将其所有孩子结点入队,输出其数据域,循环至队列为空即可。
小思考:由于是森林,因此自己指定一个根结点,一次性得到一整个二叉树存储的森林,方便遍历。
困难:
1.一开始dfs时,由于新增孩子结点需要得到最后一个孩子结点的地址,而查找每个结点的最后一个孩子结点我使用的方法是顺序往下访问直到终点,但是结果表明倒数第二个点始终超时,我最初认为是建树没有消递归导致的,解决了第一个大题之后,我受到启发,为dfs函数增加了返回值和参数,调用时传入根结点和最后一个孩子结点的地址,每次将新的孩子结点地址返回,这样寻找孩子结点的时间复杂度降低到了O(1),顺利通过。原来的做法大概是遇上了孩子结点很多的情况,不断重复做查找最后一个孩子结点导致的,最坏时间复杂度为O(n2)。
反思:
1.某些细小的程序结构可能在某些极端情况下拖累整个程序的效率,需要更注意程序整体的高效与优美。
代码实现:
#include <iostream>
#include <vector>
#include <list>
#include <deque>
using namespace std;
char* zifu;
int* dushu;
int now = 0,n = 0;
vector<int> ans;
struct Node{
char val;
struct Node* left, *right;
Node(char a) :val(a),left(NULL),right(NULL) {}
};
Node* dfs(Node *root,Node *quick) {//root为父亲,按照有儿子没儿子建当前节点,建之前讨论有兄弟没兄弟,quick是当前父亲最后一个儿子结点,没有儿子则为父亲本身
int du = dushu[now];
if (du == 0) {
Node* add = new Node(zifu[now]);
/*if (root->left == NULL) {//旧的查找孩子结点方式,低效
root->left = add;
}
else {
root = root->left;
while (root->right != NULL) { root = root->right; }
root->right = add;
}*/
if (root->left == NULL) {
quick->left = add;
}
else {
quick->right = add;
}
return add; //将新结点作返回值
}
else {
Node* myroot = new Node(zifu[now]);
/*if (root->left == NULL) {//旧的查找孩子结点方式,低效
root->left = myroot;
}
else {
root = root->left;
while (root->right != NULL){
root = root->right;
}
root->right = myroot;
}*/
if (root->left == NULL) {
quick->left = myroot;
}
else {
quick->right = myroot;
}
quick = myroot;
for (int i = 0; i < du; i++) {
now++;
quick=dfs(myroot,quick);
}
return myroot;
}
}
void travel(Node* root) {//遇到每一个节点直接把所有儿子入队
if (root == NULL) {
return;
}
Node* now = NULL;
deque<Node*> dl;
dl.push_back(root);
while (!dl.empty()) {
now = dl.front();
if (now != root) {//不输出自己指定的根结点
ans.push_back(now->val);
}
dl.pop_front();
if (now->left != NULL) {
now = now->left;
while (now != NULL) {
dl.push_back(now);
now = now->right;
}
}
}
}
int main() {
char cup='0';
scanf("%d", &n);
zifu = new char[n];
dushu = new int[n];
int i = 0;
cup = getchar();//读入快一点
while (i != n) {
cup=getchar();
if (cup != ' ') {
zifu[i++] = cup;
}
}
cup = getchar();
for (int i = 0; i < n; i++) {
scanf("%d", &dushu[i]);
}
Node* root = new Node('0');//由于是森林,用一个自己指定的根结点统一管理
Node* quick = root;
while (now != n ) {
quick=dfs(root,quick);
now++;
}
travel(root);
for (int i = 0; i < ans.size(); i++) {
putchar(ans[i]);
if (i < ans.size() - 1) {
putchar(32);
}
}
}
7-3 纸带切割 (100 分)
有一条细长的纸带,长度为 L 个单位,宽度为一个单位。现在要将纸带切割成 n 段。每次切割把当前纸带分成两段,切割位置都在整数单位上,切割代价是当前切割纸带的总长度。每次切割都选择未达最终要求的最长纸带切割,若这样的纸带有多条,则任选一条切割。如何切割,才能完成任务,并且总代价最小。
输入格式:
第1行,1个整数n,表示切割成的段数, 1≤n≤100000.
第2行,n个整数Li,用空格分隔,表示要切割成的各段的长度,1≤Li≤200000000,1≤i≤n.
输出格式:
第1行,1个整数,表示最小的总代价。
第2行,若干个整数,用空格分隔,表示总代价最小时每次切割的代价。
输入样例:
在这里给出一组输入。例如:
5
5 6 7 2 4
输出样例:
在这里给出相应的输出。例如:
54
24 13 11 6
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 100 ms
内存限制 5 MB
解法一:
思路:
解决本题关键有两点:
1.联想到哈夫曼树。
2.如何高效建立哈夫曼树。
对于第二个问题,使用优先队列将十分方便!
小细节:注意可能会溢出,使用long long。
代码实现:
#include <iostream>
#include <set>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
vector<long long> ans;
priority_queue<long long, vector<long long>,greater<long long> > pdl;
int main() {
int n = 0;
int* duan; long long cup = 0, sum = 0;
scanf("%d", &n);
duan = new int[n];
for (int i = 0; i < n; i++) {
scanf("%d", &duan[i]);
pdl.push(duan[i]);
}
for (int i = 0; i < n-1; i++) {
int cup1 = pdl.top(); pdl.pop();
int cup2 = pdl.top(); pdl.pop();
cup = cup1+cup2;
sum += cup;
pdl.push(cup);
ans.push_back(cup);
}
printf("%lld\n", sum);
while (!ans.empty()) {
printf("%lld", ans.back());
ans.pop_back();
if (ans.size() != 0) {
printf(" ");
}
}
}
7-4 序列乘积 (100 分)
两个递增序列A和B,长度都是n。令 Ai 和 Bj 做乘积,1≤i,j≤n.请输出n*n个乘积中从小到大的前n个。
输入格式:
第1行,1个整数n,表示序列的长度, 1≤n≤100000.
第2行,n个整数Ai,用空格分隔,表示序列A,1≤Ai≤40000,1≤i≤n.
第3行,n个整数Bi,用空格分隔,表示序列B,1≤Bi≤40000,1≤i≤n.
输出格式:
1行,n个整数,用空格分隔,表示序列乘积中的从小到大前n个。
输入样例:
在这里给出一组输入。例如:
5
1 3 5 7 9
2 4 6 8 10
输出样例:
在这里给出相应的输出。例如:
2 4 6 6 8
作者 谷方明
单位 吉林大学
代码长度限制 16 KB
时间限制 100 ms
内存限制 5 MB
法一(未满分):
思路:
解决本题关键有两点:
1.如何少算(降低时间复杂度)。
2.如何少存(降低空间复杂度)。
3.如何快速获得一个序列的最小元素。
对于第三个问题,若使用线性容器存储,每次都将要重新排序,时间复杂度很高,由于每次取最小,想到使用小根堆。
第一二个问题综合考虑,我观察到如果将乘积按序,列为矩阵,显然每行是递增的,每列也是递增的,而每个对角线上的元素,对应从此开始整个右下角的矩阵的最小元素,(又由于整个矩阵最小的n个元素在第一行和第一列的概率更大)于是我首先只计算第一行与第一列的所有元素,输出时若发现当前将要输出元素大于当前行列对应的对角线右下角最近的一个的元素,则计算该对角元所在行列未计算元素,将其加入小根堆,重新开始输出,输出过程中继续维持此原则,直到输出了n个元素。
问题:
1.最后一个点内存超限。
反思:
1.对于数据量太大时,即使一开始只计算第一行和第一列的元素,也会使内存翻倍,所以不行。
2.当时尝试过各种小的优化,比如每次计算到比当前最大元素还大就停止计算等,结果表明根本性问题不解决小的优化都是徒劳。
代码实现:
#include <iostream>
#include <queue>
using namespace std;
int maxs = 0;
int n = 0;
void upmax(int n) {
if (maxs < n) {
maxs = n;
}
}
priority_queue<long, vector<long>, greater<long>> pdl;
void resize(int now) {//调整大小(对内存的挣扎)
priority_queue<long, vector<long>, greater<long>> emppdl;
for (int i = now; i < n; i++) {
emppdl.push(pdl.top());
pdl.pop();
}
pdl = emppdl;
}
void renew(int district,int n,long *cup1,long *cup2) {//更新数据
long cup=0;
for (int i = district; i < n; i++) {
cup = cup1[district] * cup2[i];
if (cup > maxs) {
break;
}
if (i != district) {
upmax(cup);
pdl.push(cup);
}
}
for(int i=district;i<n;i++){
cup = cup2[district] * cup1[i];
if (cup > maxs) {
break;
}
upmax(cup);
pdl.push(cup);
}
}
int main() {
scanf("%d", &n);
long* cup1 = new long[n];
for (int i = 0; i < n; i++) {
scanf("%ld", &cup1[i]);
}
long* cup2 = new long[n];
for (int i = 0; i < n; i++) {
scanf("%ld", &cup2[i]);
}
for (int i = 0; i < n; i++) {//第一行第一列元素放入堆
long cup = cup1[0] * cup2[i];
if (i != 0) {
upmax(cup);
pdl.push(cup);
}
cup = cup2[0] * cup1[i];
upmax(cup);
pdl.push(cup);
}
resize(0);
int district = 1;
for (int i = 0; i < n; i++) {
if (pdl.top() > cup1[district] * cup2[district]) {//检查当前堆顶元素和最近未计算的对角元的大小关系
renew(district++,n,cup1,cup2);
resize(i);
}
printf("%ld", pdl.top());
if (i != n - 1) {
printf(" ");
}
pdl.pop();
}
}
解法一:
基于法一的思考和同学的提示,由于每一行或每一列都是递增的,若当前行(列)最小元未输出必不可能输出其后元素,因此第一次计算只需要计算第一行(第一列),输出时输出了一个元素,则将该行(列),的下一个元素入堆即可,这样才能保证空间复杂度合格,代码也更简单。
反思:
1.要思考更简洁的原则解题,比较复杂的原则容易产生问题,且代码也可能更复杂;本方法竟然只要30行左右。
代码实现:
#include <iostream>
#include <queue>
using namespace std;
int maxs = 0;
int n = 0;
void upmax(int n) {
if (maxs < n) {
maxs = n;
}
}
priority_queue<pair<long, pair<int, int>>,vector<pair<long, pair<int, int>>>,greater<pair<long,pair<int, int>>>> pdl;//队列中既存值,也存元素下标,方便计算本行下一个元素
int main() {
scanf("%d", &n);
long* cup1 = new long[n];//long保证不溢出
pair<long, pair<int, int>> now;
for (int i = 0; i < n; i++) {
scanf("%ld", &cup1[i]);
}
long* cup2 = new long[n];
for (int i = 0; i < n; i++) {
scanf("%ld", &cup2[i]);
}
for (int i = 0; i < n; i++) {//计算第一行
now.first=cup1[i] * cup2[0];
now.second.first = i; now.second.second = 0;
pdl.push(now);
}
int district = 1;
for (int i = 0; i < n; i++) {
now = pdl.top();
pdl.push(make_pair(cup1[now.second.first] * cup2[now.second.second+1], make_pair(now.second.first, now.second.second+1)));//计算本行下一个元素
printf("%ld", now.first);
if (i != n - 1) {
printf(" ");
}
pdl.pop();
}
}
总结:
1.要更注意题目解法的简洁性。
2.注意细节,保证程序整体的优美和高效。
3.本质性的问题很难通过细节的优化来解决,因此要把改进的重心放在对逻辑的优化上。