5490. 吃掉 N 个橘子的最少天数

一道平平无奇的记忆化搜索题目,题面已经把递归方程给我们了。

现在的问题是$n\leq 2e9$,如何实现记忆化呢?这里可以用到$unordered_map$来存储状态,因为所需要的状态远远小于$2e9$,直接开一个$2e9$大小的数组是不可能的。

code

class Solution {
public:
    unordered_map<int,int> dp;
    int f(int n)
    {
        if (n==1)
            return 1;
        if (n==2||n==3)
            return 2;
        if (dp.find(n)!=dp.end())
            return dp[n];
        int res=min(f(n/2)+n%2+1,f(n/3)+n%3+1);
        dp[n]=res;
        return res;
    }
    int minDays(int n) {
        dp.clear();
        return f(n);
    }
};

1524.和为奇数的子数组数目

题目看起来很简单,但是做起来可就不简单了

我的思路是:奇数+偶数=奇数,奇数=奇数,根据这个规律来找dp方程,想了两个dp方程都不对……

一看题解,阔然开朗。

  1. 利用前缀和的性质:$sum[i..j]=sum[j]-sum[i-1]$。
  2. 奇数-偶数=奇数,偶数-奇数=奇数。
  3. 所以我们只需要记录$[0…i-1]$的前缀和中有多少个奇数$odd$和偶数$even$。如果$sum[i]$是奇数,那么以$arr[i]$为结尾的子数组和为奇数的数目为$even$;如果$sum[i]$是偶数,则为$odd$。

代码来源于评论区

class Solution {
public:
    int numOfSubarrays(vector<int>& arr) {
        const int mod=1e9+7;
        int len = arr.size();
        int even = 1, odd = 0;  // 前缀和奇偶个数
        long res = 0, sum = 0;
        // 利用前缀和,将之前的前缀和的奇偶数量记录下来,根据当前子数组的奇偶,
        //再使用之前的更小子数组奇偶个数算出以当前数为尾的子数组的奇和个数
        for (int i = 0; i < len; i++) {
            sum += arr[i];
            if (sum % 2 == 0) {
                res += odd;
                res %= mod;
                even++;
            } else {
                res += even;
                res %= mod;
                odd++;
            }
        }
        return res;
    }
};

1521. 找到最接近目标值的函数值

当时卡在第三题了,第四题还没看过😓

后面看了一下题目,感觉跟前缀和相关,然后懒懒的我就去看题解了~

位运算

这个题解讲得很好,建议认真吸收。

根据按位与的两个性质,尤其是第二个性质,使得我们可以轻松地遍历每一个func(arr,l,r)的值,从而得到答案。

代码直接看上面题解就好,我基本照抄的。

class Solution {
public:
    int closestToTarget(vector<int>& arr, int target) {
        int ans=abs(arr[0]-target);
        vector<int> valid{arr[0]};
        for (int i=1;i<arr.size();i++)
        {
            vector<int> new_valid;
            for (int v:valid)
                new_valid.emplace_back(v&arr[i]);
            new_valid.emplace_back(arr[i]);       
            new_valid.erase(unique(new_valid.begin(),new_valid.end()),new_valid.end());
            for (int v:new_valid)
                ans=min(ans,abs(v-target));
        }
        return ans;
    }
};

st表+二分

这里同样需要用到按位与&的一个性质:$x&y \geq x$ and $x&y \geq y$。所以有单调性$func(arr,l,r)\geq func(arr,l,r+1)$。

题目要找的最小值$ans$就是最接近$target$的$func(arr,l,r)$,显然这个最小值$ans$一定出现在比$target$小的第一个$func$或比$target$大的第一个$func$。因此我们可以二分查找出比$target$小的第一个$func(arr,l,r)$,再选出$|func(arr,l,r-1)-target|$和$|func(arr,l,r)-target|$两者中的最小值。

复习一下关于st表的知识:

  • https://oi-wiki.org/ds/sparse-table/
  • st表是用于解决可重复贡献问题的数据结构。
  • 可重复贡献问题指对于运算$opt$,满足$x opt x = x$,$opt$还需要满足结合律才可用st表求解。
  • 能用st表解决的可重复贡献问题有:最大值$\max(x,x)=x$,GCD,按位与。
  • st表可以让我们在$O(1)$内求出某个区间$[L,R]$内的最大$func(arr,l,r)$ $L\leq l \leq r \leq R$。

再根据$func$的单调性,固定左端点$l$,二分查找$r$使$|func(arr,l,r)-target|$最小。枚举$l$需要$O(N)$,二分查找需要$O(\log N)$,st表查找一次需要$O(1)$,所以总的复杂度是$O(N)*O(\log N)*O(1)=O(N\log N)$。

参考题解

class Solution {
public:
    int closestToTarget(vector<int>& arr, int target) 
    {
        int Logn[100005];
        int ans=INT32_MAX;
        int n=arr.size();
        vector<vector<int>> st(n,vector<int>(20));
        for (int i=0;i<n;i++)
            st[i][0]=arr[i];
        Logn[1] = 0;
        Logn[2] = 1;
        for (int i = 3; i <= n; i++) 
            Logn[i] = Logn[i / 2] + 1;
        for (int j=1;j<20;j++)
            for (int i=0;i+(1<<j)-1<n;i++)
                st[i][j]=st[i][j-1] & st[i+(1<<(j-1))][j-1];
        // 现学的lambda function 
        auto ask = [st,Logn](int x,int y)
        {
            if (x>y)
                return INT32_MAX;
            int tmp=Logn[y-x+1];
            return st[x][tmp] & st[y-(1<<tmp)+1][tmp];
        };
        for (int l=0;l<n;l++)
        {
            int r_left=l;
            int r_right=n-1;
            while (r_left<r_right)
            {
                int mid=(r_left+r_right)/2;
                if (ask(l,mid)>=target)
                    r_left=mid+1;
                else
                    r_right=mid;
            }
            int r=r_right;
            ans=min(ans,abs(target-ask(l,r)));
            ans=min(ans,abs(target-ask(l,r-1)));
        }
        return ans;
    }
};

5466. Maximum Number of Non-Overlapping Substrings

Leetcode周赛P3,才一百多人做出来,题目很新颖,并不需要用到很高深的算法,但是对思维要求高。

  1. 一开始我是想着贪心的,我的想法是子串只包含一种字符,这样构造得到的子串应该是最多的。算法大概是枚举每一个出现的字符的右端点last[ch],向左扩展到左端点,要求这段字符串满足条件2。交上去之后发现"abab"的答案是["abab"],我的贪心是过不了这个样例的。
  2. 于是我就去想dp的做法了,dp[i]表示前$[0,i]$中的最多子串数目,$dp[i]=\max{dp[j]+1}$ 如果子串$[j+1,i]$所有出现的字符都只出现在子串中,对于所有的$i$, $dp[i]=dp[i-1]$. 可以用一个优先队列来保存之前的dp[j],每次只取$\geq dp[i-1]$的出来,检查$[j+1,i]$区间是否符合条件。然而,这样做连第一个样例都过不去😓。首先,复杂度可能会达到$O(N^2)$,其次,要得到最优解的字符串比较难。把代码放这纪念吧…
vector<string> maxNumOfSubstrings(string s)
{
    vector<string> ans;
    int n = s.size();
    vector<int> dp(n, 0);
    vector<int> fromdp(n, -1);
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
    int first[30], last[30];
    memset(first,-1,sizeof(first));
    memset(last,-1,sizeof(last));
    for (int i = 'a'; i <= 'z'; i++)
    {
        first[i - 'a'] = s.find(i);
        last[i - 'a'] = s.rfind(i);
    }
    for (int i = 0; i < n; i++)
    {
        q.push({0, i-1});
        dp[i]=i?dp[i-1]:0;
        queue<pair<int, int>> tmpq;
        while (!q.empty())
        {
            auto [pre, le] = q.top();
            q.pop();
            if (pre < dp[i])
                break;
            bool bo = 0;
            printf("%d %d %d\n",i,le,pre);
            for (int j = le + 1; j <= i; j++)
            {
                int ch = s[j] - 'a';
                if (first[ch] > le && last[ch] <= i)
                    continue;
                else
                {
                    bo = 1;
                    break;
                }
            }
            tmpq.push({pre, le});
            if (bo)
                continue;
            else
            {
                dp[i] = pre + 1;
                fromdp[i] = pre;
                q.push({dp[i], i});
                break;
            }
        }
        while (!tmpq.empty())
        {
            q.push(tmpq.front());
            tmpq.pop();
        }
    }
    for (int i=0;i<n;i++)
        cout<<dp[i]<<" ";
    for (int i = n - 1; i != -1; i = fromdp[i])
    {
        ans.push_back(s.substr(fromdp[i] + 1, i - fromdp[i]));
    }
    return ans;
}
  1. 评论区的贪心解法$O(N)$:
    1. 预处理出l[ch]表示字符ch最左侧的出现位置, r[ch]表示最右侧的出现位置。
    2. get函数固定子串的左端点start,求至少包含左端点的字符s[start]的合法子串的右端点。
    3. 遍历字符串,只找以当前字符s[i]在字符串第一次出现的字符开始的字符串,用一个神奇的变量right来记录当前答案中最右侧字符串的右端点位置。如果i>right说明已经走出了上一个字符串的范围了,可以开始下一个字符串了。
    4. 贪心的正确性证明:我不知道怎么证明😂
class Solution
{
public:
    vector<string> maxNumOfSubstrings(string s)
    {
        int l[26], r[26];
        memset(l, -1, sizeof l);
        memset(r, -1, sizeof r);
        for (int i = 0; i < s.size(); i++)
        { //记录每个字符第一次和最后一次出现的位置
            int x = s[i] - 'a';
            if (l[x] == -1)
                l[x] = i;
            r[x] = i;
        }
        auto get = [&](int start) 
        { //求start开始符合条件2的子字符串
            int end = r[s[start] - 'a'];
            for (int j = start + 1; j < end; j++)
            {
                if (l[s[j] - 'a'] < start)
                    return -1;
                end = max(end, r[s[j] - 'a']);
            }
            return end;
        };
        vector<string> res = {""};
        int right = INT_MAX;
        for (int i = 0; i < s.size(); i++)
        {
            if (i == l[s[i] - 'a'])
            { //只找以当前字符在字符串第一次出现的字符开始的子字符串,所以最多找26次
                int r = get(i);
                if (r == -1)
                    continue;
                if (i > right)
                {
                    right = r;
                    res.push_back("");
                }
                right = min(right, r);
                res.back() = s.substr(i, r - i + 1);
            }
        }
        return res;
    }
};

对计算机专业的思考

虽然我现在刚升上大二,但是我接触计算机专业已经快四年了。我对这个专业以及行业还是有自己的理解的。以下均为个人见解,仅供参考。

先说结论吧:计算机专业很好找工作,工资水平在各行业中也是数一数二的,我特别推荐自学能力强的同学来读。

计算机专业学的到底是什么?

我的理解:学的是一种操控计算机的思维模式。

本科核心课程:C/C++语言,算法与数据结构,计算机组成与结构,数据库,计算机网络,编译原理等等。
各个大学的计算机专业修读计划都差不多的,大家感兴趣的话可以在大学的官网上面找,这里展示港中深的核心课程:
CS Major Required Course

想必核心课程的名字大家看得云里雾里的,上网搜一搜也不太清楚究竟学的是什么,这里我用大白话解释一下吧:

  • C/C++语言:跟学英语类似,需要掌握一些语法,更重要的是要理解计算机组成与结构(类比通过学英语来了解西方文化)。
  • 算法与数据结构:与数学联系紧密,培养逻辑思维与推理能力,需要多刷算法题。
  • 计算机组成与结构:像一门文科,知识点很多,要记/理解很多东西。
  • 数据库:也挺像文科的,根据一些理论来设计数据库。
  • 计算机网络:文科,没怎么了解过……
  • 编译原理:这个了解就更少了,需要对算法与数据结构和计算机组成与结构有一定的了解。

如果按照文理科的分类方式,计算机专业处在文科和理科的中间吧。

读计算机专业需要什么条件?

有小伙伴可能会想:我的电脑水平很一般啊,之前也从未接触过编程,那我学起来会不会很吃力?

其实不会的,从零开始学,大学四年足够了。我个人觉得学计算机专业不太需要天赋,更多是靠后天的勤奋。编程能力不行,就多写代码;理论知识不过关,就多看书。而且这个专业特别公平,你会就是会,不会就是不会,很容易测试出一个人专业水平的高低。

接触过编程的同学就更好啦,能更快地上手。没有接触过也没关系的,大一多写点代码,不要满足于课内的东西(出来找工作不够用),很快就能追上来了。

P.S. 程序员最好的语言是英语,因为要经常查google。

我了解过的大学

当时我是先选专业,再选大学的,一定要读计算机专业。据我所知,以下大学的计算机专业都不错的:

  • 电子科技大学:性价比极高,省排名10000开外都有机会考上,培养了很多互联网人才,技术氛围浓厚。
  • 北京邮电大学:优势是在北京,分数要求不太高。
  • 武汉大学:信息安全专业了解一下,也算是计算机专业的一个分支了。
  • 华中科技大学:跟武大差不多吧,我当时的第二志愿。
  • 南京大学:听说人工智能很强(不过跟本科生关系不大)
  • 北京航空航天大学:分数要求和学科排名都很高。
  • 清华大学:考的上的都不用我多说了吧…

然而,我去了香港中文大学(深圳)。
一年后再看嘛,这学校的缺点是学费贵(四年无奖学金,所有费用50w+)和不能保研;优点是容易申请国外学校和国际化教育。如果同学们家里条件允许还是可以考虑一下的~

就读一年的个人体验

由于我第二年才分专业,第一年我上的计算机专业课程并不多:python语言,程序设计基础,算法与数据结构,数据库,其中后面两个核心课程都是我提前上的。

相比于课内学到的知识,我课外学的知识要多得多。很多需要用到的知识/技能是没有对应的课程的,比如:开发一个完整的网站,为了通过面试刷算法题,用markdown语言写文档等等。

我喜欢这个专业的一个重要原因就是学习资源实在太多了,无论你想学哪个方面的知识,一搜就有了。而且有很多程序员都是非计算机科班毕业的,他们靠自学也能进入行业,由此可见自学的重要性。

就业需要掌握什么技能?

本科就业多数是去开发岗,开发岗可分为前端和后端,其中前端是用户直接能看到的界面,后端则是处理数据。

如今的入职面试多是技术面试:现场做算法题+知识点问答。

  • 在面试中解算法题,解释自己的思路,甚至将完整代码写出。
  • 知识点问答则考验你对于一些语言/知识的理解。

据我了解,具备以下条件:熟练掌握一门语言,平时多刷算法题,有实习经历,有项目经历,基本就能进大公司(阿里,腾讯,字节跳动等等)。

算法岗(人工智能)的话,在本科应该只有少数大佬能去吧,毕竟这需要科研成果或者竞赛获奖。

行业状况如何?

整个互联网行业从目前来看,发展空间还是不小的,如果能找准一家正处于上升期的公司,实现财务自由也不是不可能(当然这个很看运气)。进入互联网大公司当然也不错,国内互联网大厂应届生年薪20w起步,就是加班有点多而已……

很多人觉得程序员竞争很大,三十多岁的程序员很容易被裁员,然后就找不到工作了。的确,互联网行业竞争很大,如果不能持续学习新技术/知识,就很容易被淘汰。但如果你能保持对技术的热情,那到哪里都是香饽饽。国外的程序员大牛五六十岁仍在持续输出,更证明了程序员并不是一个吃青春饭的职业。

互联网行业加班多,程序员容易秃头,这些都是现实。996也基本是国内很多互联网公司的常态了。只能说国内竞争太激烈了吧,相比于国内,美国/加拿大的程序员加班还是很少的。如果不想让工作占据生活,可以选择在国内的外企或者出国工作,选择工作负担相对轻一些的国内企业也可以。

本科毕业是就业还是读研?

现在读研的人越来越多咯,我现在是打算就业,不过以后可能会改啦。

我现在对读研的理解:

  • 推迟就业,延缓大四的就业压力。
  • 提高学历,更容易找到工作。
  • 走科研路线:读研/读博->研究员/大学教授。

如果是打算做开发岗的话,那应该就不需要读研了,感觉有点浪费时间了,读研学到的东西对工作帮助不大。

如果是想做人工智能方面的工作,可以说是一定要读研了,不管是就业还是进一步科研,都需要一定的学术成果。

专业,城市,学校三者的优先级如何?

我个人认为:专业>城市>学校,当然这跟我专业目标明确也有很大关系。

很多同学应该是没有明确自己想读的专业的,那城市>学校>=专业也可。

报志愿这件事嘛,父母长辈的意见都是参考而已,最后还是得靠自己做决定。其实就算选了一个不合适自己的也没太大关系,大二转专业就行了。从这个角度来看,专业的优先级是最低的,应该先选城市和学校。其实出成绩后你会发现,学校也没有几家可以选的,选分数高的很难被录取,选分数低的又觉得浪费了自己的分数,所以一般都是选自己分数段上下的几间学校。这几间分数相差不多的学校实力也差不多,最后就变成了选城市。

前几天看到一个讲的不错的视频,推荐大家去看一看,还可以去评论区看看对各专业的吐槽:https://www.bilibili.com/video/BV1hK411H7bY

总结

本人意见仅供参考,大家还是要多了解专业,城市,学校的信息,毕竟报志愿和高考同样重要噢。

Google Kick Start Round D

这次成绩是三次以来最好的啦,1332名,虽然我只做出了第一题,后面三题都是暴力过第一个数据点~

Record Breaker

送分模拟题,注意一下边界条件即可。
a[n+1],premax都初始化为-1,这样就不需要特判边界了。

int a[200005];
void solve()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    int premax = -1;
    int ans = 0;
    a[n + 1] = -1;
    for (int i = 1; i <= n; i++)
    {
        if (a[i] > premax && a[i] > a[i + 1])
            ++ans;
        premax = max(premax, a[i]);
    }
    cout << ans << endl;
}

Alien Piano

比赛时看这题觉得很复杂,不知道用什么方法来构造这个序列。于是就跳过去了,后面回来把暴力$O(4^N)$写了:

暴力

int a[15];
int b[15];
int ans;
void dfs(int cur, int n, int res)
{
    if (cur > n)
    {
        int cnt = 0;
        for (int i = 1; i < n; i++)
        {
            if ((a[i] < a[i + 1] && b[i] < b[i + 1]) || (a[i] > a[i + 1] && b[i] > b[i + 1]) || (a[i] == a[i + 1] && b[i] == b[i + 1]))
                continue;
            ++cnt;
        }
        ans = min(ans, cnt);
        return;
    }
    for (int i = 1; i <= 4; i++)
    {
        b[cur] = i;
        dfs(cur + 1, n, res);
    }
}
void solve()
{
    int k;
    cin >> k;
    ans = k;
    for (int i = 1; i <= k; i++)
        scanf("%d", &a[i]);
    dfs(1, k, 0);
    cout << ans << endl;
}

看完题解觉得还好,有两种方法:

贪心

  • 直接找长度大于$4$的连续上升/下降子序列,每有一个这样的子序列,就要打破一次规则
  • 我当时怎么没想到呢?
void solve()
{
    int k;
    cin>>k;
    for (int i=1;i<=k;i++)
        cin>>a[i];
    int ans = 0,cnt1 = 1,cnt2 = 1;
    for(int i = 2; i <= k; i++){
        if( a[i - 1] < a[i] ){
            cnt1++;
            cnt2 = 1;
        }
        else if(a[i - 1] > a[i]){
            cnt2++;
            cnt1 = 1;
        }
        if(cnt1 > 4 || cnt2 > 4){
            cnt1 = cnt2 = 1;
            ans++;
        }
    }
    cout << ans << '\n';
}

dp

  • $dp(i,j)$表示前$i$个note,其中第$i$个note用$j$来表示的最小规则打破数。
  • $$ dp(i,j)=\min{dp(i-1,j’)+P(i,j’,j)|1 \leq j’ \leq 4} $$
  • $P(i,j’,j)$表示:第$i-1$个note用$j’$来表示,第$i$个note用$j$来表示,需要打破的规则数。
void solve()
{
    int k;
    cin >> k;
    for (int i = 1; i <= k; i++)
        scanf("%d", &a[i]);
    for (int j = 1; j <= 4; j++)
        dp[1][j] = 0;
    for (int i = 2; i <= k; i++)
        for (int j = 1; j <= 4; j++)
        {
            dp[i][j] = INT32_MAX;
            for (int k = 1; k <= 4; k++)
            {
                int p;
                if ((k < j && a[i - 1] < a[i]) || (k > j && a[i - 1] > a[i]) || (k == j && a[i - 1] == a[i]))
                    p = 0;
                else
                    p = 1;
                dp[i][j] = min(dp[i][j], dp[i - 1][k] + p);
            }
        }
    int ans = k;
    for (int j = 1; j <= 4; j++)
        ans = min(ans, dp[k][j]);
    cout << ans << endl;
}

Beauty of tree

这题一开始没审清楚题目,travels up the tree应该是向上找父节点,我以为是向上找第A层的随便一个节点都行,白做了大半个小时😓,后来只能用暴力咯。

暴力

  • 要用printf("%f")来输出double类型,用cout似乎精度有点问题,过不了第一个测试集。
  • 遍历每一对选择的节点(i,j)时,ij都要1 to n
int f[105];
bool visited[105];
void solve()
{
    int n, a, b;
    cin >> n >> a >> b;
    for (int i = 1; i <= n; i++)
        f[i] = i;
    for (int i = 2; i <= n; i++)
    {
        int x;
        scanf("%d", &x);
        f[i] = x;
    }
    ll ans = 0;
    int tot = 0;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        // choose node i,j
        {
            ++tot;
            int x = i;
            int y = j;
            for (int k = 1; k <= n; k++)
                visited[k] = 0;
            if (x == y)
                ++ans;
            else
                ans += 2;
            visited[x] = 1;
            visited[y] = 1;
            int cnt = 0;
            while (x != f[x])
            {
                x = f[x];
                ++cnt;
                if (cnt % a == 0 && !visited[x])
                {
                    visited[x] = 1;
                    ++ans;
                }
            }
            cnt = 0;
            while (y != f[y])
            {
                y = f[y];
                ++cnt;
                if (cnt % b == 0 && !visited[y])
                {
                    visited[y] = 1;
                    ++ans;
                }
            }
        }
    }
    printf("%f\n", (double)ans / tot);
}

dfs

原来dfs还有这个用处啊,学到了学到了。

  • P(being visited by either Amadea or Bilva) = P(visited by Amadea) + P(visited by Bilva) - (P(visited by Amadea)*P(visited by Bilva))
  • P(visited by Amadea) = visited times / total number of nodes
  • 所以只需要算每个节点访问过的次数即可。
  • 首先,每个节点至少访问过一次,因为至少会被选到一次。
  • 其次,越靠近叶子节点,访问次数会越多。
  • 每遍历到一个点,就给它的第A/B个父节点加上它自己的访问次数。
  • 用一个辅助数组path来记录从根节点1到当前节点u经过的所有节点
class Solution
{
    vector<int> a_visit, b_visit;
    vector<vector<int>> children;
    vector<int> path;
    int n, a, b;

    void dfs(int u)
    {
        path.emplace_back(u);
        a_visit[u] = 1;
        b_visit[u] = 1;
        for (int v : children[u])
            dfs(v);
        path.pop_back();
        if (path.size() >= a)
            a_visit[path[path.size() - a]] += a_visit[u];
        if (path.size() >= b)
            b_visit[path[path.size() - b]] += b_visit[u];
    }

public:
    void solve()
    {
        cin >> n >> a >> b;
        children = vector<vector<int>>(n + 1);
        a_visit = vector<int>(n + 1);
        b_visit = vector<int>(n + 1);
        for (int i = 2; i <= n; i++)
        {
            int x;
            scanf("%d", &x);
            children[x].push_back(i);
        }
        dfs(1);
        double ans = 0;
        for (int i = 1; i <= n; i++)
        {
            double pa = (double)a_visit[i] / n;
            double pb = (double)b_visit[i] / n;
            ans += pa + pb - pa * pb;
        }
        printf("%f\n", ans);
    }
};

Locked Doors

做到这题的时候没时间了,只剩下半小时不到,在最后一分钟的时候才过了第一个样例😄。

模拟

该题的模拟并不难想,只需用两个指针lr来维护左右锁住的门的下标即可,复杂度为$O(NQ)$

  • $N$个房间只有$N-1$个门,注意数组大小。
  • 边界条件:a[0],a[n]=INT_MAX,当一侧的门全部打开后,只能打开另一侧的门。
  • 画图来帮助理解门和房间的对应关系
void solve()
{
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= n - 1; i++)
        scanf("%d", &a[i]);
    a[0] = INT32_MAX, a[n] = INT32_MAX;
    while (q--)
    {
        int s, k;
        scanf("%d %d", &s, &k);
        int l = s - 1, r = s;
        for (int i = 2; i <= k; i++)
        {
            if (a[l] < a[r])
            {
                --l;
                s = l + 1;
            }
            else
            {
                ++r;
                s = r;
            }
        }
        printf("%d ", s);
    }
}

总结

做了几次Google的比赛了,题风跟别处还是有很大不同的:

  • 注重边界条件的考察
  • 不会告诉你哪个数据点错了,只能自己想程序中不完善的地方
  • 分析题目的能力 > 积累算法的多少

5211. 概率最大的路径

dfs

我第一反应是用dfs,无奈dfs怎么优化都超时,后来看了评论区才知道,加上if (curp<1e-4) return;就可以过了,原理也很简单:只要答案与标准答案的误差不超过 1e-5 ,就会被视作正确答案,所以如果当前已经走过的路径的成功概率太小的话,可以直接不要这条路径。

class Solution {
public:
    vector<pair<int,double>> e[10005];
    bool visited[10005];
    double ans;
    double laste;
    void dfs(int u,int end,double curp)
    {
        if (u==end)
        {
            ans=max(ans,curp);
            return;
        }
        if (curp<1e-5||curp*laste<ans)
            return;
        for (int i=0;i<e[u].size();i++)
        {
            int v=e[u][i].first;
            if (visited[v])
                continue;
            double p=e[u][i].second;
            visited[v]=1;
            dfs(v,end,curp*p);
            visited[v]=0;
        }
    }
    double maxProbability(int n, vector<vector<int>>& edges, vector<double>& s, int start, int end) {
        for (int i=0;i<edges.size();i++)
        {
            if (s[i]>0)
            {
            e[edges[i][0]].push_back(make_pair(edges[i][1],s[i]));
            e[edges[i][1]].push_back(make_pair(edges[i][0],s[i]));
            }
        }
        for (int i=0;i<e[end].size();i++)
            laste=max(laste,e[end][i].second);
        dfs(start,end,1);
        return ans;
    }
};

dijkstra

dfs算法超时后,我立刻就想到了最短路算法:dijkstra。

这题与最短路本质上是一样的,最短路是将每条边加起来,求最小边权和;而这题是将每条边乘起来,求最大边权乘积。
证明方法:

但是我一开始的dijkstra写错了,优先队列中的节点按照外部数组dis从大到小排列,这样会导致一个问题:

  • 队列里的元素的优先级是由外界的dis变化决定的,priority_queue就无法实时保证队列里的元素满足堆性质了。我只要在外面随便改一改dis的值,priority_queue就废了。
  • https://www.luogu.com.cn/discuss/show/51361

所以priority_queue中存储的应该是pair<double,int> {p,u},优先队列中的元素优先级不依赖于外部元素。

此处不需要重载运算符:

  • 优先队列默认的比较类型是:class Compare = std::less<typename Container::value_type>
  • less会调用类型 T 上的 operator< ,除非特化。
  • pair<class T1, class T2>的小于号<会先比较第一个元素.first,再比较第二个元素.second
  • 所以priority_queue<pair<double,int>> q是按p从大到小排列。
class Solution {
public:
    vector<pair<double,int>> e[10005];
    bool visited[10005];
    double maxProbability(int n, vector<vector<int>>& edges, vector<double>& s, int start, int end) {
        for (int i=0;i<edges.size();i++)
        {
            e[edges[i][0]].push_back({s[i],edges[i][1]});
            e[edges[i][1]].push_back({s[i],edges[i][0]});
        }
        vector<double> dis(n+1,0);
        priority_queue<pair<double,int>> q;
        dis[start]=1;
        q.push({1.0,start});
        while (!q.empty())
        {
            auto [p,u]=q.top();
            q.pop();
            if (visited[u])
                continue;
            visited[u]=1;
            for (auto [p,v]:e[u])
            {
                dis[v]=max(dis[v],dis[u]*p);
                if (!visited[v])
                    q.push({dis[v],v});
            }
        }
        return dis[end];
    }
};

108. 将有序数组转换为二叉搜索树,这是一道有趣的树上递归题

  1. 乍一看,这题不很简单嘛,直接递推就好了,nums[mid]的左边是左子树,右边是右子树,最终树的形状是一个人字形
  2. 原来这个条件本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。每个节点都要满足,那应该要用上递归了。
  3. 递归算法的设计:
    1. 其实蛮套路的,树上递归的题目代码都很短,基本就是处理左子树–>处理右子树–>合并左右子树
    2. 我是先用直觉写出递归,然后手推一下样例看看对不对,发现问题后再修改递归
    3. 每个$[L,R]$区间的处理方式与$[0,nums.size()-1]$是一样的,容易想到是以mid=(l+r)/2为分界,将当前树分成左右两棵子树,最后再考虑边界情况即可
  4. 代码:
class Solution {
public:
    TreeNode *build(vector<int> &nums,int l,int r)
    {
        if (l>r)
            return nullptr;
        int mid=(l+r)/2;
        TreeNode *cur=new TreeNode(nums[mid]);
        cur->left=build(nums,l,mid-1);
        cur->right=build(nums,mid+1,r);
        return cur;
    }
    TreeNode* sortedArrayToBST(vector<int>& nums) 
    {
        return build(nums,0,nums.size()-1);
    }
};

递归的代码看起来真的爽👍

6.22-6.28 Learning Notes

Leetcode

MST

请你找到给定图中最小生成树的所有关键边和伪关键边。如果最小生成树中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。

题目数据范围很小,可以暴力判断每条边是关键边/伪关键边/其他的。

[参考题解

  1. 先求MST。
  2. 对于原图中的每条边:
    1. 如果边在MST中,计算去掉该边后的MST权值,如果比原来的大,那么当前边是伪关键边;如果相等,那么它是关键边。
    2. 如果边不在MST中,计算强制加入当前边后的MST权值,如果相等,则当前边是伪关键边。
Read more »
0%