算法提升(8):路灯问题(dp)、先序中序写后序、完全二叉树的节点个数、最长递增子序列、被3整除
题目1
小Q正在给一条长度为n的道路设计路灯安置方案。为了让问题更简单,小Q把道路视为n个方格,需要照亮的地方用'.'表示,不需要照亮的障碍物格子用'X'表示。小Q现在要在道路上设置一些路灯,对于安置在pos位置的路灯,这盏路灯可以照亮pos - 1,pos,pos+ 1这三个位置。小Q希望能安置尽量少的路灯照亮所有'.'区域,希望你能帮他计算一下最少需要多少盏路灯。
输入描述:
输入的第一行包含一个正整数t(1 <= t < 1000),表示测试用例数,接下来每两行一个测试数据,第一行一个正整数n(1 <= n <= 1000),表示道路的长度。第二行一个字符串s表示道路的构造,只包含'.'和'X'。
输出描述:
对于每个测试用例,输出一个正整数表示最少需要多少盏路灯。
思路
贪心策略,保证当前位置pos决定需要的一盏灯(不一定放在pos位置)不会对下一个循环要讨论的位置产生影响。字符串从左到右遍历,遇到'X'不放灯,pos位置往后移。遇到'.'必放一个灯,但是不一定是当前位置放,先判断下一个位置是否越界,如果越界代表灯放完了,可以break了。如果没有越界,判断下一个位置是否是'X',如果是,当前位置已经放过灯了,下一个位置是X,不用放,所以我来到pos+2位置;如果下一个位置不是'X',那么我之前决定一定要放的灯其实是放在pos+1位置上,这样不管pos+2是不是'X',pos、pos+1、pos+2位置都符合要求了,所以我来到pos+3位置。按照以上的规则遍历str直到pos越界。这种贪心一定要保证当前位置pos决定需要的一盏灯(不一定放在pos位置)不会对下一个循环要讨论的位置产生影响,否则你如果要考虑前面的影响就会没完没了。
int minLight(string str)
{
if (str.size() < 1)
{
return 0;
}
int light = 0;
int pos = 0;
while (pos < str.size())
{
if (str[pos] == 'X')
{
pos++;
}
else
{
light++;
if (pos + 1 == str.size())
{
break;
}
else
{
if (str[pos + 1] == 'X')
{
pos = pos + 2;
}
else
{
pos = pos + 3;
}
}
}
}
return light;
}
题目2
已知一棵二叉树中没有重复节点,并且给定了这棵树的中序遍历数组和先序遍历数组,返回后序遍历数组。
比如给定:
int[] pre={1, 2, 4, 5, 3, 6, 7 };
int[] in={4, 2, 5, 1, 6, 3, 7};
返回:
思路
递归思路,先从先序遍历中拿到第一个元素,这个元素一定是头节点,是后序遍历中最后一个位置的值,然后在中序遍历中找到这个元素,中序遍历中的这个元素左边一定是左子树,记住这个长度l,右边一定是右子树,记住这个长度r,在先序遍历中开头元素后面一位开始数l一定是左子树,数完左子树剩下的r就是右子树。在后序遍历中从开头数l一定是左子树,从l后面一位数r就是右子树。在后序遍历数组中填好头节点递归再去算两个子树。base case是数组长度是1,就一个节点直接填,或者开头指针大于结尾指针直接返回
void process(vector<int> pre, vector<int> in, int preL, int preR, int inL, int inR, int postL, int postR, vector<int>& post, unordered_map<int, int>& indexMap);
vector<int> getPostOrder(vector<int> pre, vector<int> in)
{
if (pre.size() == 0 || in.size() == 0)
{
return {};
}
vector<int> post(pre.size());
unordered_map<int, int> indexMap; //记录中序遍历中各节点的下标,递归中有在中序遍历数组中查找头节点下标的行为,这样可以省去for循环
for (int i = 0; i < in.size(); i++)
{
indexMap.insert(make_pair(in[i], i));
}
process(pre, in, 0, pre.size() - 1, 0, in.size() - 1, 0, post.size() - 1, post, indexMap);
return post;
}
void process(vector<int> pre, vector<int> in, int preL, int preR, int inL, int inR, int postL, int postR, vector<int>& post, unordered_map<int, int> &indexMap)
{
if (preL > preR)
{
return;
}
if (preL == preR)
{
post[postL] = pre[preL];
return;
}
int head = pre[preL];
int inHeadIndex = indexMap.at(head);
post[postR] = head;
process(pre, in, preL + 1, preL + inHeadIndex - inL, inL, inHeadIndex - 1, postL, postL + inHeadIndex - inL - 1, post, indexMap);
process(pre, in, preL + inHeadIndex - inL + 1, preR, inHeadIndex + 1, inR, postL + inHeadIndex - inL, postR - 1, post, indexMap);
}
题目3
求完全二叉树节点的个数
思路
先求这个完全二叉树的深度h,然后看根节点的右子树的最左节点有没有到达深度h(即右子树的深度有没有h-1)。如果有,则证明根节点的左子树是满二叉树,且深度为h-1;如果没有,则证明左子树不是满二叉树,而右子树是高度为h-2的满二叉树。如果有就移动到左子树的根节点重复前面的过程,如果没有就移动到左子树的根节点重复前面的过程,直到到达叶子节点。因为满二叉树直到高度就可以求节点个数,所以根据上面的流程可以求出整棵树节点的个数。
int mostLeftNodeLevel(TreeNode* head, int level);
int process(TreeNode* head, int level, int h);
int getCompeteBTNodes(TreeNode* head)
{
if (head == nullptr)
{
return 0;
}
return process(head, 1, mostLeftNodeLevel(head, 1));
}
int process(TreeNode* head, int level, int h) //level为head在h中所处的深度。h是整个完全二叉树的深度,它不会变
{
if (head->left == nullptr)
{
return 1;
}
if (mostLeftNodeLevel(head->right, level + 1) == h)
{
return (1 << (h - level)) + process(head->right, level + 1, h); //左子树是满二叉树,左子树的节点数2^h-level^-1 + 头节点 + 右子树的节点数
}
else
{
return (1 << (h - level - 1)) + process(head->left, level + 1, h); //右子树是满二叉树,右子树的节点数2^h-level^-1 + 头节点 + 右子树的节点数
}
}
int mostLeftNodeLevel(TreeNode* head, int level) //计算以head为根节点的树在整个完全二叉树的深度h中所处的深度,level为head在h中所处的深度
{
while (head != nullptr)
{
head = head->left;
level++;
}
return level - 1;
}
题目4
最长递增子序列问题。给定一个数组arr,找出它最长的递增子序列,并返回长度
思路
优化前:分别算出以每个位置为结尾的最长递增子序列。当前位置为i,找i之前比i位置上的数小的数中递增子序列最长的那个,它的长度再加1就是i位置的最长递增子序列。但是找i之前比i位置上的数小的数中递增子序列最长的那个需要枚举,从0开始一直找到i-1位置,这导致整个算法的时间复杂度是O(N2),能不能优化这种算法。
优化后算法:准备一个长度与arr一样的数组ends,这个数组中存的值代表:最长递增子序列长度为i+1的所有子序列中,结尾数字最小的那个子序列末尾的数。一开始ends里有效区看作为0,当遍历数组arr来到i位置时,在ends数组有效区中二分查找比arr[i]大的最左边的数。如果没找到,就扩充有效区,把新扩充的位置存上arr[i]的值,以arr[i]结尾的最长递增子序列的长度就是新扩充的位置下标+1。如果找到了,就把找到的位置更新成arr[i],以arr[i]结尾的最长递增子序列的长度就是找到的位置下标+1。直到遍历完arr数组,就能得到最长的递增子序列。
为什么要像上面那样安排,利用ends数组。我们可以从ends数组的组成看出它是从大到小有序的数组,一旦有序就可以使用二分查找,将时间复杂度降低。优化前的思路是一个dp,而dp中有枚举行为,我们可以利用构建单调性降阶或直接省去枚举行为,这里就是构建单调性降阶,这是一种很好的思路。使用优化后的思路,整体的时间复杂度会降低到O(NlogN)。那么为什么ends数组会是一种有效的解。因为题干中要求的是递增,在更新时我们是把拥有相同长度子序列的末尾从大的更新成小的,最小的末尾有利于数组中后序的值再接到它的后面。详细讲解见https://www.bilibili.com/video/BV13g41157hK?p=27&vd_source=77d06bb648c4cce91c6939baa0595bcd P27 01:31:50
int find(vector<int> ends, int ava, int tar);
int lengthOfLIS(vector<int> arr)
{
if (arr.size() == 0)
{
return 0;
}
vector<int> ends(arr.size());
int ava = 0;
ends[0] = arr[0];
for (int i = 1; i < arr.size(); i++)
{
int pos = find(ends, ava, arr[i]);
if (pos > ava)
{
ends[++ava] = arr[i];
}
else
{
ends[pos] = arr[i];
}
}
return ava + 1;
}
int find(vector<int> ends, int ava, int tar)
{
int front = 0;
int back = ava;
int mid;
while (front <= back)
{
mid = (front - back) / 2 + back;
if (tar > ends[mid])
{
front = mid + 1;
}
else if (tar < ends[mid])
{
back = mid - 1;
}
else
{
return mid;
}
}
return front;
}
题目5
小Q得到一个神奇的数列: 1, 12, 123,...,12345678910,1234567891011...
并且小Q对于能否被3整除这个性质很感兴趣。
小Q现在希望你能帮他计算一下从数列的第L个到第R个(包含端点)有多少个数可以被3整除。
输入描述:
输入包括两个整数L和R(1 <= L <= R <= 1e9),表示要求解的区间两端。
输出描述:
输出一个整数,表示区间内能被3整除的数字个数。
示例1:
输入
2 5
输出
3
思路
判断一个数能不能被3整除,等价于一个数的每位之和能否被3整除。所以只用判断(n*(n+1)/2)%3即可。因为数量太大了,所以用long long
int res(int L, int R)
{
int ans = 0;
for (long long i = L; i <= R; i++)
{
if (i == 1)
{
continue;
}
if ((1 + i) * i / 2 % 3 == 0)
{
ans++;
}
}
return ans;
}
浙公网安备 33010602011771号