Leetcode练习题:复杂数据结构
复杂数据结构这里主要学习了字典树、并查集和线段树,这些数据结构在解决专门的问题十分方便,有时甚至有其他方法是无法解决的。字典树比较简单,好理解,主要用于字符串查找的问题。并查集是刚学的,但是实际含义也是简单的,可能难在如何进行优化。线段树的题目都是难题,概念也是相对好理解,就是应用上还存在困难。
这些复杂的数据结构,学习了之后,能够熟悉使用还需要练习和时间。
字典树,利用了字符串的相同前缀来快速查找字符串,插入和查找比较常见。简单的来说,节点的层数代表了在字符串中的下标,每一个节点有所有情况字符的子节点,如果存在即为1,否则为空
struct TrieTree{
bool is_end;
vector<TrieTree*> child;
TrieTree():is_end(false),child(128,nullptr){}
void insert(string &s)
{
TrieTree *root=this;
for(char c:s)
{
if(!root->child[c])
{
root->child[c]=new TrieTree();
}
root=root->child[c];
}
root->is_end=true;
}
bool find(string &s)
{
TrieTree *root=this;
for(char c:s)
{
if(!root->child[c])
{
return false;
}
root=root->child[c];
}
return root->is_end;
}
};
并查集,用于数据量大且问题是寻找元素从属集合的问题。
常规步骤是:首先让每一个元素自己构成一个集合,集合数位n,然后按照条件或顺序开始合并同一个集合的元素,在合并的时候用到了查找元素所属集合的操作,合并成功后集合数要-1。
对于并查集,原理其实很简单,但是我总是会混乱find和parent,但是仔细想想也会清楚,因此还没有设计路径优化的内容。
class DSU{
public:
int *parent;
int count;
//初始化
DSU(int n):parent(new int[n]),count(n)
{
for(int i=0;i<n;i++)
{
parent[i]=i;
}
}
//查找所属集合 并更新
int find(int a)
{
if(a!=parent[a])
{
parent[a]=find(parent[a]);
}
return parent[a];
}
//合并
void union_t(int a,int b)
{
//如果不相同再Union
if(find(a)!=find(b))
{
parent[find(a)]=find(b);
count--;
}
}
};
线段树,实际上就是一颗二叉排序树,或者说二叉搜索树。但就是每个节点不是一个数了而是一个区间,整颗树可以将区间划分成单元区间,每个单元区间对应一个叶子节点。概念是很清晰了,但是实际应用起来还是很有困难。不过代码实现是有很多选择的,不是采用的树的结构,而是二维数组来实现的较多,还是看下面的题目就好。
主要操作有构造和查询,一般递归实现,根据题目不同,函数建立不同。
720:词典中最长的单词
问题描述
给出一个字符串数组words组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。
若无答案,则返回空字符串。
示例 1:
输入:
words = [“w”,“wo”,“wor”,“worl”, “world”]
输出:“world”
解释:
单词"world"可由"w", “wo”, “wor”, 和
“worl"添加一个字母组成。第一个单词是"w”,该单词只有一个字母。我们需要从一个字母的单词开始,每步添加一个字母。
示例 2:
输入:
words = [“a”, “banana”, “app”, “appl”, “ap”, “apply”, “apple”]
输出:“apple”
解释:
“apply"和"apple"都能由词典中的单词组成。但是"apple"的字典序小于"apply”。
解题思路
用字典树来解决就十分简单了,用直接的方法好了,首先将所有字符串插入,然后开始查找每一个字符串,如果长度大于最长的就更新。
代码实现
class TrieTree
{
public:
bool is_end;
vector<TrieTree*> child;
TrieTree():is_end(false),child(26){};
static void insert(TrieTree* t,const string &s)
{
for(char c:s)
{
int pos=c-'a';
if(!t->child[pos])
{
t->child[pos]=new TrieTree();
}
t=t->child[pos];
}
t->is_end=true;
}
static bool search(TrieTree* t,const string &s)
{
for(char c:s)
{
int pos=c-'a';
if(!t->child[pos]->is_end)
{
return false;
}
t=t->child[pos];
}
return t->is_end;
}
};
string longestWord(vector<string>& words)
{
TrieTree *root=new TrieTree();
for(string &s:words)
{
TrieTree::insert(root,s);
}
string longest;
for(string &s:words)
{
if(TrieTree::search(root,s))
{
if(s.size()>longest.size())
{
longest=s;
}else if(s.size()==longest.size()&&s<longest)
{
longest=s;
}
}
}
return longest;
}
反思与收获
使用指针数组来实现,因为有很多字符情况。
插入时,如果该字符节点存在,则继续往下搜索。如果该字符节点不存在就创建。
查找时,如果该字符节点存在,则继续往下搜索,如果不存在直接返回,最后记得是返回该节点的Is_end属性
1023:驼峰式匹配
问题描述
如果我们可以将小写字母插入模式串 pattern 得到待查询项 query,那么待查询项与给定模式串匹配。(我们可以在任何位置插入每个字符,也可以插入 0 个字符。)
给定待查询列表 queries,和模式串 pattern,返回由布尔值组成的答案列表 answer。只有在待查项 queries[i] 与模式串 pattern 匹配时, answer[i] 才为 true,否则为 false。
示例 1:
输入:pattern = “FB”,queries =
[“FooBar”,“FooBarTest”,“FootBall”,“FrameBuffer”,“ForceFeedBack”]输出:[true,false,true,true,false]
解释:
“FooBar” 可以这样生成:“F” + “oo” + “B” + “ar”。
“FootBall” 可以这样生成:“F” + “oot” + “B” + “all”.
“FrameBuffer” 可以这样生成:“F” + “rame” + “B” + “uffer”.
示例 2:
输入:pattern = “FoBa”,queries =
[“FooBar”,“FooBarTest”,“FootBall”,“FrameBuffer”,“ForceFeedBack”]输出:[true,false,true,false,false]
解释:
“FooBar” 可以这样生成:“Fo” + “o” + “Ba” + “r”.
“FootBall” 可以这样生成:“Fo” + “ot” + “Ba” + “ll”.
示例 3:
输出:pattern = “FoBaT”,queries =
[“FooBar”,“FooBarTest”,“FootBall”,“FrameBuffer”,“ForceFeedBack”]输入:[false,true,false,false,false]
解释:
“FooBarTest” 可以这样生成:“Fo” + “o” + “Ba” + “r” + “T” + “est”
解题思路
将pattern看成插入到字典树的字符串,每一次匹配过程实际上就是查找的过程。
因为小写字母是可以插入的,所以如果查找时小写字母不存在的话,是可以忽略的,指针也不往下走。
但是如果大写字母不存在的话,直接返回false了。因此只需改写find函数。
代码实现
struct TrieTree{
bool is_end;
vector<TrieTree*> child;
TrieTree():is_end(false),child(128,nullptr){}
void insert(string &s)
{
TrieTree *root=this;
for(char c:s)
{
if(!root->child[c])
{
root->child[c]=new TrieTree();
}
root=root->child[c];
}
root->is_end=true;
}
bool find(string &s)
{
TrieTree *root=this;
for(char c:s)
{
if(c>96)
{
if(!root->child[c])
{
continue;
}
root=root->child[c];
}
else
{
if(!root->child[c])
{
return false;
}
root=root->child[c];
}
}
return root->is_end;
}
};
vector<bool> camelMatch(vector<string>& queries,string pattern)
{
TrieTree *t=new TrieTree;
t->insert(pattern);
vector<bool> ans;
//cout<<queries.size()<<endl;
for(string &s:queries)
{
ans.push_back(t->find(s));
}
return ans;
}
反思与收获
遇到小写字母不匹配跳过的意思是在字符串的下标中往后走一步,跟字典树这边在走是没有关系的,指针并没有往下走进行匹配 pattern Fo 匹配F的话 也会失败,因为最后返回的是节点的is_end属性,并没有以F结尾。
字典树的变形,改写了find函数。
问题描述
在英语中,我们有一个叫做 词根(root)的概念,它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。
现在,给定一个由许多词根组成的词典和一个句子。你需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。
你需要输出替换之后的句子。
示例 1:
输入:dictionary = [“cat”,“bat”,“rat”], sentence = “the cattle was
rattled by the battery”输出:“the cat was rat by the bat”
示例 2:
输入:dictionary = [“a”,“b”,“c”], sentence = “aadsfasf absbs bbab
cadsfafs”输出:“a a b c”
示例 3:
输入:dictionary = [“a”, “aa”, “aaa”, “aaaa”], sentence = “a aa a aaaa
aaa aaa aaa aaaaaa bbb baba ababa”输出:“a a a a a a a a bbb baba a”
示例 4:
输入:dictionary = [“catt”,“cat”,“bat”,“rat”], sentence = “the cattle was
rattled by the battery”输出:“the cat was rat by the bat”
示例 5:
输入:dictionary = [“ac”,“ab”], sentence = “it is abnormal that this
solution is accepted”输出:“it is ab that this solution is ac”
解题思路
这题对于字符串的判定其实跟上一题也差不多,首先将词典都插入到字典树中,扫描整个sentence,找到最短的词根,进行变形。主要是没有替换成功的字符串要保持原样。所以没有使用find函数,而是利用循环进行扫描,记录下起始和结束的下标。
代码实现
struct TrieTree{
bool is_end;
vector<TrieTree*> child;
TrieTree():is_end(false),child(128,nullptr){}
void insert(string &s)
{
TrieTree *root=this;
for(char c:s)
{
if(!root->child[c])
{
root->child[c]=new TrieTree();
}
root=root->child[c];
}
root->is_end=true;
}
};
string replaceWords(vector<string> &dict,string sentence)
{
TrieTree *t=new TrieTree;
TrieTree *node;
for(string &s:dict)
{
t->insert(s);
}
string ans;
int start=0,end=0;
for(int i=0;i<sentence.size();i++)
{
if(sentence[i]==' ')
{
continue;
}
//每次都从字典树顶端开始找起
node=t;
//记录起始下标
start=i;
while(i<sentence.size()&&sentence[i]!=' ')
{
//如果找到最后了,或者后面不匹配了
//找到的肯定是第一个最短的词根
if(node->is_end||!node->child[sentence[i]])
{
break;
}
node=node->child[sentence[i]];
i++;
}
//记录结束下标
end=i;
//把这个单词扫描完
while(i<sentence.size()&&sentence[i]!=' ')
{
i++;
}
//如果是没有词根的,那就是一整个原来的单词
if(!node->is_end)
{
end=i;
}
//保存结果
ans+=sentence.substr(start,end-start)+" ";
}
//将最后一个空格去除
if(!ans.empty())
{
ans.pop_back();
}
return ans;
}
反思与收获
字典树本身就是利用了大量字符串公共前缀,从而实现了高效的查找效率。这个概念跟英文的词根是很类似的,词根也可以看成是前缀。
947:移除最多的同行或同列石头
问题描述
我们将石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
每次 move 操作都会移除一块所在行或者列上有其他石头存在的石头。
请你设计一个算法,计算最多能执行多少次 move 操作?
示例 1:
输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
输出:5
示例 2:
输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]
输出:3
示例 3:
输入:stones = [[0,0]]
输出:0
解题思路
在同一行或者是同一列的元素相当于是在一个集合当中,每次移除可以拿出一个,所以答案即为总数-集合数
考虑使用并查集来解决,只要在同一行或者同一列则为一个集合。首先初始化集合,大小为n,每个元素组成一个集合 parent[i]=i
之后判断,进行union(i,j)的操作
代码实现
class DSU{
public:
int *parent;
int count;
DSU(int n):parent(new int[n]),count(n)
{
for(int i=0;i<n;i++)
{
parent[i]=i;
}
}
int find(int a)
{
if(a!=parent[a])
{
parent[a]=find(parent[a]);
}
return parent[a];
}
void union_t(int a,int b)
{
if(find(a)!=find(b))
{
parent[find(a)]=find(b);
count--;
}
}
};
class Solution
{
public:
int removeStones(vector<vector<int>>& stones)
{
int n=stones.size();
if(!n)
{
return 0;
}
DSU s(n);
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
if(stones[i][0]==stones[j][0]||stones[i][1]==stones[j][1])
{
s.union_t(i,j);
}
}
}
return n-s.count;
}
};
反思与收获
积累DSU的实现代码,和初始化、查找、合并的基本操作。
问题描述
在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\ 或空格构成。这些字符会将方块划分为一些共边的区域。
(请注意,反斜杠字符是转义的,因此输入的字符串中是 \,在代码中函数调用时传递的实参用 “\” 表示一个""。)。
返回区域的数目。
示例 1:
输入:
[
" /",
"/ "
]
输出:2
解释:2x2 网格如下:
示例 2:
输入:
[
" /",
" "
]
输出:1
解释:2x2 网格如下:
示例 3:
输入:
[
“/”,
“/”
]
输出:4
解释:(回想一下,因为 \ 字符是转义的,所以 “\/” 表示 /,而 “/\” 表示 /\。)
2x2 网格如下:
示例 4:
输入:
[
“/”,
“/”
]
输出:5
解释:(回想一下,因为 \ 字符是转义的,所以 “/\” 表示 /\,而 “\/” 表示 /。)
2x2 网格如下:
示例 5:
输入:
[
“//”,
"/ "
]
输出:3
解释:2x2 网格如下:
解题思路
区域划分,但是区域面积或者范围是很难表示的,更别说分割或者合并。这时候就想到其实每一块区域,都是由所有的点围起来的。
最开始的时候,是外面所有的点为围起了区域,这些点都是一个集合的。
这时加入一条边,这条边有两个点组成。如果这两个合并不成功,则说明这两个点本身就属于一个集合的,并且围起来了,那区域个数就++。
如果这两个点合并成功了,那就新增了一条边,还需要别的边来围成区域。
重点就是点的坐标和方块下标的转换。
代码实现
class Solution
{
public:
vector<int> parent;
int find(int x)
{
if(x!=parent[x])
{
parent[x]=find(parent[x]);
}
return parent[x];
}
int union_t(int a,int b)
{
if(find(a)!=find(b))
{
parent[find(a)]=find(b);
return 1;
}
else
{
//无需合并
return 0;
}
}
int regionsBySlashes(vector<string>& grid)
{
int n=grid.size();
parent=vector<int>((n+1)*(n+1),0);
for(int i=0;i<(n+1)*(n+1);i++)
{
parent[i]=i;
}
//把最外面的四条大边连起来
for(int i=0;i<n;i++)
{
//up
union_t(i,i+1);
//left
union_t(i*(n+1),(i+1)*(n+1));
//down
union_t(n*(n+1)+i,n*(n+1)+i+1);
//right
union_t(i*(n+1)+n,(i+1)*(n+1)+n);
}
int count=1;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
if(grid[i][j]==' ')
{
continue;
}
int u,v;
if(grid[i][j]=='/')
{
//右上点
u=i*(n+1)+j+1;
//左下点
v=(i+1)*(n+1)+j;
}else
{
// '\'左上点
u=i*(n+1)+j;
//右下点
v=(i+1)*(n+1)+j+1;
}
if(!union_t(u,v))
{
count++;
}
}
}
return count;
}
};
反思与收获
用围成的点来表示区域,如果点都在一个集合里,说明是有边将其相连的,如果这时再加入一条边,能让它们再次相连那就围成区域了。这个解法十分巧妙,完全利用了并查集的知识来解决复杂的区域面积分割问题。
1202:交换字符串中的元素
问题描述
给你一个字符串 s,以及该字符串中的一些「索引对」数组 pairs,其中 pairs[i] = [a, b] 表示字符串中的两个索引(编号从 0 开始)。
你可以 任意多次交换 在 pairs 中任意一对索引处的字符。
返回在经过若干次交换后,s 可以变成的按字典序最小的字符串。
示例 1:
输入:s = “dcab”, pairs = [[0,3],[1,2]]
输出:“bacd”
解释:
交换 s[0] 和 s[3], s = “bcad”
交换 s[1] 和 s[2], s = “bacd”
示例 2:
输入:s = “dcab”, pairs = [[0,3],[1,2],[0,2]]
输出:“abcd”
解释:
交换 s[0] 和 s[3], s = “bcad”
交换 s[0] 和 s[2], s = “acbd”
交换 s[1] 和 s[2], s = “abcd”
示例 3:
输入:s = “cba”, pairs = [[0,1],[1,2]]
输出:“abc”
解释:
交换 s[0] 和 s[1], s = “bca”
交换 s[1] 和 s[2], s = “bac”
交换 s[0] 和 s[1], s = “abc”
解题思路
能交换位置的就是属于同一个集合,把能互相交换的位置的放在一起,保存每组可相互交换的所有元素,然后将其排序,从最小的开始选即可。
代码实现
int parent[100001];
int find(int x)
{
if(x!=parent[x])
{
parent[x]=find(parent[x]);
}
return parent[x];
}
string smallestStringWithSwaps(string s,vector<vector<int>> &pairs)
{
int n=s.size();
//初始化
for(int i=0;i<n;i++)
{
parent[i]=i;
}
//union 将能交换位置的合并
for(auto item:pairs)
{
parent[find(item[0])]=find(item[1]);
}
//创建可交换的各个组
vector<vector<char>> v(n);
//找到该元素从属集合组,将该元素push
for(int i=0;i<n;i++)
{
v[find(i)].push_back(s[i]);
}
string ans;
for(int i=0;i<n;i++)
{
//逆序排序,为了后面是back(),pop_back()的操作
sort(v[i].rbegin(),v[i].rend());
}
for(int i=0;i<n;i++)
{
//找到该位置从属集合
int pos=find(i);
//从最小的开始op
ans+=v[pos].back();
v[pos].pop_back();
}
return ans;
}
反思与收获
sort默认升序,使用sort(v[i].rbegin(),v[i].rend());
可以降序排序。
使用完并查集时候,可以利用二维数组存储每个集合的所有元素,进行一些操作比如排序等。
1157:子数组中占绝大多数的元素
问题描述
实现一个 MajorityChecker 的类,它具有下述两个 API:
1、MajorityChecker(int[] arr) :用给定的数组 arr 来构造一个 MajorityChecker 的实例。
2、int query(int left, int right, int threshold)
参数为:
0 <= left <= right < arr.length ,其中arr.length表示数组 arr 的长度。
2 * threshold > right - left + 1,也就是说阈值 threshold 始终比子序列长度的一半还要大。
功能:每次查询返回在 arr[left], arr[left+1], …, arr[right] 中至少出现threshold 次数的元素,如果不存在这样的元素,就返回 -1。
说明:由于threshold 比子序列长度的一半还要大,因此,不可能存在两个元素都出现了threshold 次。
示例:
MajorityChecker majorityChecker = new MajorityChecker([1,1,2,2,1,1]);
majorityChecker.query(0,5,4); // 返回 1
majorityChecker.query(0,3,3); // 返回 -1
majorityChecker.query(2,3,2); // 返回 2
解题思路
因为是多次区间搜索,所以考虑构造线段树。
每个节点除了表示区间外,肯定要保存众数的信息,但是在向上合并的时候,并不能确定哪个是众数,情况太多种了,在这一段是众数,但加上另外一段的信息时就不能保证了。
查看题解
了解学习了摩尔投票,主要思想就是抵消,解决找众数的问题。
candidate存可能的众数,count记录当前有多少票。
int majorityElement(vector<int>& nums) {
int candidate = 0,cnt = 0;
for(int num:nums){
if(cnt == 0) {candidate = nums;++cnt};
else(num == candidate) ? ++cnt : --cnt;
}
}
所以每个节点相当于存的是可能的众数,以及摩尔投票后还剩下的票数。因此合并的时候,如果两个区间可能众数相同,则count相加,如果不同的话,就保存较大的数,以及count大-count小。
再最后进行二分查找,判断是否为众数。
使用upper_bound lower_bound
代码实现
//每个节点存储摩尔投票后的 可能众数和剩下的票数
struct node{
int val;
int count;
node(int val=0,int count=0):val(val),count(count){}
};
//重载+,实现节点之间的相加
node operator+(const node &a,const node &b)
{
node res=a;
if(res.val==b.val)
{
res.count+=b.count;
}else if(res.count>b.count)
{
res.count-=b.count;
}else
{
res.val=b.val;
res.count=b.count-res.count;
}
//保存较大的数的信息,和抵消后的票数
return res;
}
class MajorityChecker {
private:
vector<vector<int>> pos;
vector<node> segements;
int L,R;
public:
//递归构造线段树
void build(int root,int left, int right, vector<int>&nums)
{
//如果叶子节点构造
if(left==right)
{
segements[root]={nums[left],1};
return;
}
int mid=left+(right-left)/2;
//先构造左右子节点
build(root*2+1,left,mid,nums);
build(root*2+2,mid+1,right,nums);
//该节点结果为左右子节点相加
segements[root]=segements[root*2+1]+segements[root*2+2];
return;
}
//查找
node query(int root,int left,int right, int qleft,int qright)
{
//查找区间不在该区间范围
if(qright<left||qleft>right)
{
return{0,0};
}
//查找范围包含该区间范围
if(qleft<=left&&right<=qright)
{
return segements[root];
}
//范围相交 继续缩小范围
int mid=left+(right-left)/2;
return query(root*2+1,left,mid,qleft,qright)+query(root*2+2,mid+1,right,qleft,qright);
}
MajorityChecker(vector<int>& arr)
{
//初始化
pos.resize(20001,vector<int>());
int n=arr.size();
L=0;
R=n-1;
segements.resize(n*4,node(0,0));
build(0,L,R,arr);
//记录每个相同元素出现的下标
for(int i=0;i<n;i++)
{
pos[arr[i]].push_back(i);
}
}
int query(int left, int right, int threshold) {
node ans=query(0,L,R,left,right);
int target=ans.val;
int count=ans.count;
if(count>=threshold)
{
return target;
}
//第一次大于右边界出现的下标-第一个大于等于左边界出现的下标 找不到的话是可以返回越界下标的
int num=upper_bound(pos[target].begin(),pos[target].end(),right)-lower_bound(pos[target].begin(),pos[target].end(),left);
if(num>=threshold)
{
return target;
}
return -1;
}
};
反思与收获
upper_bound lower_bound运用挺广泛的,然后找不到的话是可以返回越界的下标的。这题还学习了摩尔投票,最后再查找来判断是否为众数。线段树主要是left,right,qleft,qright。根据范围的不同情况就行查找的操作,这是在query函数中的。
850:矩形面积 II
问题描述
我们给出了一个(轴对齐的)矩形列表 rectangles 。 对于 rectangle[i] = [x1, y1, x2, y2],其中(x1,y1)是矩形 i 左下角的坐标,(x2,y2)是该矩形右上角的坐标。
找出平面中所有矩形叠加覆盖后的总面积。 由于答案可能太大,请返回它对 10 ^ 9 + 7 取模的结果。
示例 1:
输入:[[0,0,2,2],[1,0,2,3],[1,0,3,1]]
输出:6
解释:如图所示。
示例 2:
输入:[[0,0,1000000000,1000000000]]
输出:49
解释:答案是 10^18 对 (10^9 + 7) 取模的结果, 即 49 。
解题思路
代码实现
反思与收获
不会…看题解也很难理解,线段树本身就挺难,再加上坐标压缩和取模,有点混乱
699:掉落的方块
问题描述
在无限长的数轴(即 x 轴)上,我们根据给定的顺序放置对应的正方形方块。
第 i 个掉落的方块(positions[i] = (left, side_length))是正方形,其中 left 表示该方块最左边的点位置(positions[i][0]),side_length 表示该方块的边长(positions[i][1])。
每个方块的底部边缘平行于数轴(即 x 轴),并且从一个比目前所有的落地方块更高的高度掉落而下。在上一个方块结束掉落,并保持静止后,才开始掉落新方块。
方块的底边具有非常大的粘性,并将保持固定在它们所接触的任何长度表面上(无论是数轴还是其他方块)。邻接掉落的边不会过早地粘合在一起,因为只有底边才具有粘性。
返回一个堆叠高度列表 ans 。每一个堆叠高度 ans[i] 表示在通过 positions[0], positions[1], …, positions[i] 表示的方块掉落结束后,目前所有已经落稳的方块堆叠的最高高度。
示例 1:
输入: [[1, 2], [2, 3], [6, 1]]
输出: [2, 5, 5]
解释:
第一个方块 positions[0] = [1, 2] 掉落:
_aa
_aa
方块最大高度为 2 。
图中,a表示被方块占住的区域,a前面的下划线表示空白区域。
第二个方块 positions[1] = [2, 3] 掉落:
__aaa
__aaa
__aaa
aa_
aa_
方块最大高度为5。
大的方块保持在较小的方块的顶部,不论它的重心在哪里,因为方块的底部边缘有非常大的粘性。
第三个方块 positions[1] = [6, 1] 掉落:
__aaa
__aaa
__aaa
_aa
_aa___a
方块最大高度为5。
因此,我们返回结果[2, 5, 5]。
示例 2:
输入: [[100, 100], [200, 100]]
输出: [100, 100]
解释: 相邻的方块不会过早地卡住,只有它们的底部边缘才能粘在表面上。
解题思路
用线段树来维护该区间最高的高度值,参考题解
mx存的是该线段里的最高值,下降一个正方形就进行一次更新,找到当前的最高值,然后再更新该区域的值。
代码实现
map<int,int> mp;
const int MAX=2009;
int mx[4*2009];
//更新最大值
void update(int root,int left,int right, int qleft, int qright,int val)
{
if(left==right)
{
mx[root]=val;
return ;
}
int mid=(left+right)/2;
if(qleft<=mid)
{
update(root*2,left,mid,qleft,qright,val);
}
if(qright>mid)
{
update(root*2+1,mid+1,right,qleft,qright,val);
}
//存左右节点最大的
mx[root]=max(mx[root*2],mx[root*2+1]);
}
//查询该区间的最大值
int query(int root,int left,int right, int qleft,int qright)
{
//覆盖
if(qleft<=left&&qright>=right)
{
return mx[root];
}
int mid=(left+right)/2;
int res=0;
if(qleft<=mid)
{
res=max(res,query(root*2,left,mid,qleft,qright));
}
if(qright>mid)
{
res=max(res,query(root*2+1,mid+1,right,qleft,qright));
}
return res;
}
vector<int> fallingSquares(vector<vector<int>>& positions)
{
int n=positions.size();
for(int i=0;i<n;i++)
{
mp[positions[i][0]]=1;
mp[positions[i][0]+positions[i][1]-1]=1;
}
//进行坐标压缩
int count=0;
for(auto&it: mp)
{
it.second=count++;
}
int left,right;
int temp;
vector<int> ans;
for(int i=0;i<n;i++)
{
left=mp[positions[i][0]];
right=mp[positions[i][0]+positions[i][1]-1];
cout<<left<<" "<<right;
temp=query(1,1,2009,left,right);
cout<<" "<<temp;
update(1,1,2009,left,right,temp+positions[i][1]);
cout<<" "<<mx[1]<<endl;
ans.push_back(mx[1]);
}
return ans;
}
};
反思与收获
这题基本看不懂哈,这个代码的坐标压缩还没有看懂,但似乎是常规的坐标压缩方法,跟题目的指示是不可能将x作为线段来看的,只能将左右边界投影一下。
218:天际线问题
问题描述
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线(图B)。
Buildings Skyline Contour
每个建筑物的几何信息用三元组 [Li,Ri,Hi] 表示,其中 Li 和 Ri 分别是第 i 座建筑物左右边缘的 x 坐标,Hi 是其高度。可以保证 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX 和 Ri - Li > 0。您可以假设所有建筑物都是在绝对平坦且高度为 0 的表面上的完美矩形。
例如,图A中所有建筑物的尺寸记录为:[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] 。
输出是以 [ [x1,y1], [x2, y2], [x3, y3], … ] 格式的“关键点”(图B中的红点)的列表,它们唯一地定义了天际线。关键点是水平线段的左端点。请注意,最右侧建筑物的最后一个关键点仅用于标记天际线的终点,并始终为零高度。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
例如,图B中的天际线应该表示为:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ]。
解题思路
这题没有使用线段树,而是使用了扫描的方法,参考题解最巧妙的一点就是设计了正负数来表示区分一个建筑的左右端点。
首先把每一对pair的情况放入集合中,如果是左端点,则将高度暂输为负数,右端点正常。multiset会自动排序,先按左端点排序,后按右端点排序,都是从小到大。
开始进行扫描,如果进来的是左端点,则插入
如果是右端点,则说明该建筑结束,将height中该数值删去。
找到height中的最大值,如果跟之前存着的最大值一样,则不变,否则就是发生变化,需要将这个点记录,并更新保存值。
代码实现
vector<vector<int>> getSkyline(vector<vector<int>>& buildings)
{
multiset<pair<int,int>> st;
vector<vector<int>> ans;
//插入各点坐标与高度
for(auto& it:buildings)
{
st.insert(make_pair(it[0],-it[2]));
st.insert(make_pair(it[1],it[2]));
}
//存储高度
multiset<int> heights({0});
//记录前点
vector<int> pre={0,0};
for(auto& it:st)
{
//如果是左边点,插入高度
if(it.second<0)
{
heights.insert(-it.second);
}else
{
//如果是右边点,擦去高度
heights.erase(heights.find(it.second));
}
//找到进行端点更新后的当前最大高度值
auto maxHeight=*heights.rbegin();
//如果不相同则发生变化
if(pre[1]!=maxHeight)
{
pre[0]=it.first;
pre[1]=maxHeight;
ans.push_back(pre);
}
}
return ans;
}
反思与收获
使用负值学习到了,即区分了左右端点,又依旧保存了高度。主要是保存最高水平的值,如果扫描该点之后,发生了变化,那就是说明有了折点,无论是向上还是向下都需要记录。注意0,0是要提前加入的,这样子最后也会有保存。
也要看清题意昂,不是保存所有描绘整个建筑群的轮廓,而是只有变化了天际线由题目定义的。
——————————————————————————————————————
这三个复杂数据结构,在学习之前是没怎么听说过的也没有实现操作过。学习了之后,以后能不能真的运用可能还是个问题,字典树比较好理解,代码也好写,并查集经过几道题的训练,也搞清楚了基本套路。就是线段树有点难度,主要是一般代码都会比较长,可能只是知道了这个原理,真的要实现解决复杂的问题的时候还是不行。这里还涉及很多没有学习过的知识,下次可以从线段树的简单题开始做起,而不是一上来就难。o(╥﹏╥)o