算法提升(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;
}
posted @ 2022-08-14 21:24  小肉包i  阅读(120)  评论(0)    收藏  举报