((蓝桥杯 刷题全集)【备战(蓝桥杯)算法竞赛-第2天】( 从头开始重新做题,记录备战竞赛路上的每一道题 )距离蓝桥杯还有65天

news2024/12/24 9:54:59

🏆🏆🏆🏆🏆🏆🏆
欢迎观看我的博客,如有问题交流,欢迎评论区留言,一定尽快回复!(大家可以去看我的专栏,是所有文章的目录)
 
文章字体风格:
红色文字表示:重难点✔★
蓝色文字表示:思路以及想法✔★
 
如果大家觉得有帮助的话,感谢大家帮忙
点赞!收藏!转发!

 
我的qq号是:1210931886,欢迎大家加群,一起学习,互相交流,共同进步🎉🎉✨✨
🥇🥇🥇🥇🥇🥇🥇

蓝桥杯系列,为大家提供

  1. 做题全集,备战蓝桥杯,就做这个系列的题即可
  2. 一个大概的做题规划——大家最好在此基础上提前两个月准备

备战蓝桥杯就刷这些题
第一天博客链接
第二天博客链接

备战(蓝桥杯)算法竞赛-第2天

        • 3. 差分(9分钟)
            • 二刷总结
        • 4. 差分矩阵(12分钟)
  • 六、双指针
        • ★ 1. 最长连续不重复子序列(20分钟)
            • 二刷总结(以空间换时间)
        • 2. 数组元素的目标和(7分钟)
        • 3. 判断子序列(8分钟)
  • 七、二进制
        • 1. 位运算算法(2分钟)
            • 返回n的最后一位1:lowbit(n) = n & -n
            • 一共有多少1 : while n = n ^(n & -n)或者 n -= n & -n
  • 八、离散化
        • ★1. 区间和(20分钟)
            • ★二刷总结
  • 九、区间合并
        • 1. 区间合并(7分钟)
  • 1. 单链表模板
        • 1. 单链表(7分钟)
  • 2. 双链表模板
        • 1. 双链表
  • 3. 模拟栈
        • 1. 模拟栈
        • 2. 表达式求值(20分钟)
  • 4. 队列 tt = -1,hh = 0;
        • 1. 模拟队列
  • 5. 单调栈
        • 1. 单调栈(4分钟)
  • 6. 单调队列
        • 1. 滑动窗口例题(10分钟)
  • 7. KMP
        • 1. KMP字符串(10分钟)
          • 二刷体会
            • ★三刷体会 ne表示算上第一个和最后一个的前缀后缀相等值
  • 8. Trie树
        • 1. Trie字符串统计(20分钟)
            • 二刷总结
        • ★2. 最大异或对
            • ★二刷总结
  • 9. 并查集 find merge
      • 1. 合并集合(5分钟)
      • 2. 连通块中点的数量(每个集合有多少个元素)
      • ★3. 食物链
          • 法一: x,x+n,x+n+n merge(f[x+n],f[x])
            • 二刷总结
          • 法二:将有关系的都存储在一个部落,用到根节点的距离表示关系
            • 二刷总结
  • 10. 堆 ✔ ✔
      • 模板
      • 1. 堆排序 ✔ ✔
      • 2. 模拟堆 ✔ ✔
  • 11. 哈希表 ✔ ✔
        • 模板
      • 例1. 模拟散列表 ✔ ✔
          • 拉链法代码(链表)
          • 开放寻址法代码(有空位就存,空位用null=0x3f3f3f3f)
      • ★例2. 字符串哈希 ✔ ✔
          • 二刷思路

3. 差分(9分钟)

二刷总结
  1. 由差分求s时,是要有数据连续性的,前面的改变了,要保证对应后面的也跟着

原题链接

#include<iostream>

using namespace std;

const int N = 1e5+10;

int a[N],s[N];

int main()
{
    int n,m;
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++) cin >> s[i];
    
    for(int i = 1; i <= n; i++)
    {
        a[i] = s[i] - s[i-1];
    }
    
    while(m--)
    {
        int l,r,c;
        cin >> l >> r >> c;
        a[l] += c;
        a[r+1] -= c;
    }
    
    for(int i = 1; i <= n; i++)
    {
        a[i] = a[i] + a[i-1];
        cout << a[i] << ' ';
    }
    
    return 0;
}

4. 差分矩阵(12分钟)

原题链接

#include<iostream>

using namespace std;

const int N = 1010;

int a[N][N],s[N][N];

int main()
{
    int n,m,q;
    cin >> n >> m >> q;
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) cin >> s[i][j];
        
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            a[i][j] = s[i][j] - s[i-1][j] - s[i][j-1] + s[i-1][j-1];

    while(q--)
    {
        int x1,y1,x2,y2,c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        a[x1][y1] += c;
        a[x1][y2+1] -= c;
        a[x2+1][y1] -= c;
        a[x2+1][y2+1] += c;
    }
            
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            a[i][j] = a[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            cout << a[i][j] << ' ';
            if(j==m)
            cout << endl;
        }
    
    return 0;
}

六、双指针

★ 1. 最长连续不重复子序列(20分钟)

二刷总结(以空间换时间)

原题链接

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int q[N], s[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    int res = 0;
    for (int i = 0, j = 0; i < n; i ++ )
    {
        s[q[i]] ++ ;
        while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;
        res = max(res, i - j + 1);
    }

    cout << res << endl;

    return 0;
}

2. 数组元素的目标和(7分钟)

原题链接

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int n, m, x;
int a[N], b[N];

int main()
{
    scanf("%d%d%d", &n, &m, &x);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);

    for (int i = 0, j = m - 1; i < n; i ++ )
    {
        while (j >= 0 && a[i] + b[j] > x) j -- ;
        if (j >= 0 && a[i] + b[j] == x) cout << i << ' ' << j << endl;
    }

    return 0;
}

3. 判断子序列(8分钟)

原题链接

#include<iostream>

using namespace std;

const int N = 1e5+10;

int a[N],b[N];
int n,m;

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i <= m; i++) cin >> b[i];
    
    int i,j;
    for(i = 1, j = 1;  i <= m; i++)
    {
        if(a[j] == b[i])
            j++;
        if(j==n+1)
            break;
    }
    //cout << j << ' ' << n << endl;
    if(j==n+1)
        cout << "Yes";
    else
        cout << "No";
    
    return 0;
}

七、二进制

最全二进制算法总结

1. 位运算算法(2分钟)

返回n的最后一位1:lowbit(n) = n & -n
一共有多少1 : while n = n ^(n & -n)或者 n -= n & -n

原题链接

#include<iostream>
using namespace std;


int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        int x;
        cin >> x;
        int res = 0;
        while(x)
        {
            x = x ^ (x&-x);
            res++;
        }
        cout << res << ' ';
    }
    return 0;
}

八、离散化

sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());

★1. 区间和(20分钟)

原题链接

★二刷总结
  1. 用数组存储数组边界值,如需要用二分查找
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

typedef pair<int,int> PII;

const int N = 300010;
int a[N],s[N];

vector<int> alls;
vector<PII> add;
int find(int x)
{
    int l = 0,r = alls.size()-1;
    while(l<r)
    {
        int mid = l + r >> 1;
        if(alls[mid]>=x) r = mid;
        else l = mid + 1;
    }
    return l+1;
}

int main()
{
    int n,m;
    cin >> n >> m;
    

    for(int i = 0; i < n; i++)
    {
        int x,c;
        cin >> x >> c;
        add.push_back({x,c});
        alls.push_back(x);
    }
    
    vector<PII> query;
    for(int i = 0; i < m; i++)
    {
        int l,r;
        cin >> l >> r;
        query.push_back({l,r});
        alls.push_back(l);
        alls.push_back(r);
    }
    
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(),alls.end()),alls.end());
    
    
    for(auto item : add)
    {
        int x = find(item.first);
        a[x] += item.second;
    }
    
    for(int i = 1; i <= alls.size(); i++) s[i] = a[i] + s[i-1];
    
    for(auto item : query)
    {
        cout << s[find(item.second)] - s[find(item.first)-1] << endl;
    }
    
    return 0;
}

九、区间合并

1. 区间合并(7分钟)

贪心做法

#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

typedef pair<int,int> PII;

vector<PII> a;

int main()
{
    int n;
    cin >> n;
    
    int l,r;
    for(int i = 0; i < n; i++) cin >> l >> r, a.push_back({l,r});
    
    sort(a.begin(),a.end());
    
    int ed = -0x3f3f3f3f;
    
    int cnt = 0;
    for(auto x : a)
    {
        if(ed<x.first)
        {
            cnt++;
            ed = max(ed,x.second);
        }
        else
        {
            ed = max(ed,x.second);
        }
    }
    cout << cnt;
    return 0;
}

数据结构

        • 3. 差分(9分钟)
            • 二刷总结
        • 4. 差分矩阵(12分钟)
  • 六、双指针
        • ★ 1. 最长连续不重复子序列(20分钟)
            • 二刷总结(以空间换时间)
        • 2. 数组元素的目标和(7分钟)
        • 3. 判断子序列(8分钟)
  • 七、二进制
        • 1. 位运算算法(2分钟)
            • 返回n的最后一位1:lowbit(n) = n & -n
            • 一共有多少1 : while n = n ^(n & -n)或者 n -= n & -n
  • 八、离散化
        • ★1. 区间和(20分钟)
            • ★二刷总结
  • 九、区间合并
        • 1. 区间合并(7分钟)
  • 1. 单链表模板
        • 1. 单链表(7分钟)
  • 2. 双链表模板
        • 1. 双链表
  • 3. 模拟栈
        • 1. 模拟栈
        • 2. 表达式求值(20分钟)
  • 4. 队列 tt = -1,hh = 0;
        • 1. 模拟队列
  • 5. 单调栈
        • 1. 单调栈(4分钟)
  • 6. 单调队列
        • 1. 滑动窗口例题(10分钟)
  • 7. KMP
        • 1. KMP字符串(10分钟)
          • 二刷体会
            • ★三刷体会 ne表示算上第一个和最后一个的前缀后缀相等值
  • 8. Trie树
        • 1. Trie字符串统计(20分钟)
            • 二刷总结
        • ★2. 最大异或对
            • ★二刷总结
  • 9. 并查集 find merge
      • 1. 合并集合(5分钟)
      • 2. 连通块中点的数量(每个集合有多少个元素)
      • ★3. 食物链
          • 法一: x,x+n,x+n+n merge(f[x+n],f[x])
            • 二刷总结
          • 法二:将有关系的都存储在一个部落,用到根节点的距离表示关系
            • 二刷总结
  • 10. 堆 ✔ ✔
      • 模板
      • 1. 堆排序 ✔ ✔
      • 2. 模拟堆 ✔ ✔
  • 11. 哈希表 ✔ ✔
        • 模板
      • 例1. 模拟散列表 ✔ ✔
          • 拉链法代码(链表)
          • 开放寻址法代码(有空位就存,空位用null=0x3f3f3f3f)
      • ★例2. 字符串哈希 ✔ ✔
          • 二刷思路

1. 单链表模板

1. 单链表(7分钟)

原题链接

#include <iostream>

using namespace std;

const int N = 100010;


// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 将x插到头结点
void add_to_head(int x)
{
    e[idx] = x, ne[idx] = head, head = idx ++ ;
}

// 将x插到下标是k的点后面
void add(int k, int x)
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}

// 将下标是k的点后面的点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int m;
    cin >> m;

    init();

    while (m -- )
    {
        int k, x;
        char op;

        cin >> op;
        if (op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }
        else if (op == 'D')
        {
            cin >> k;
            if (!k) head = ne[head];
            else remove(k - 1);
        }
        else
        {
            cin >> k >> x;
            add(k - 1, x);
        }
    }

    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}

2. 双链表模板

void init()
{
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

1. 双链表

原题链接

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int e[N], l[N], r[N], idx;

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

int main()
{
    cin >> m;

    // 0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;

    while (m -- )
    {
        string op;
        cin >> op;
        int k, x;
        if (op == "L")
        {
            cin >> x;
            insert(0, x);
        }
        else if (op == "R")
        {
            cin >> x;
            insert(l[1], x);
        }
        else if (op == "D")
        {
            cin >> k;
            remove(k + 1);
        }
        else if (op == "IL")
        {
            cin >> k >> x;
            insert(l[k + 1], x);
        }
        else
        {
            cin >> k >> x;
            insert(k + 1, x);
        }
    }

    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}

3. 模拟栈

1. 模拟栈

原题链接

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int stk[N], tt;

int main()
{
    cin >> m;
    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            stk[ ++ tt] = x;
        }
        else if (op == "pop") tt -- ;
        else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
        else cout << stk[tt] << endl;
    }

    return 0;
}

2. 表达式求值(20分钟)

原题链接

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>

using namespace std;

stack<int> num;
stack<char> op;

void eval()
{
    auto b = num.top(); num.pop();
    auto a = num.top(); num.pop();
    auto c = op.top(); op.pop();
    int x;
    if (c == '+') x = a + b;
    else if (c == '-') x = a - b;
    else if (c == '*') x = a * b;
    else x = a / b;
    num.push(x);
}

int main()
{
    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
    string str;
    cin >> str;
    for (int i = 0; i < str.size(); i ++ )
    {
        auto c = str[i];
        if (c>='0' && c <='9')
        {
            int x = 0, j = i;
            while (j < str.size() && isdigit(str[j]))
                x = x * 10 + str[j ++ ] - '0';
            i = j - 1;
            num.push(x);
        }
        else if (c == '(') op.push(c);
        else if (c == ')')
        {
            while (op.top() != '(') eval();
            op.pop();
        }
        else
        {
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[c]) eval();
            op.push(c);
        }
    }
    while (op.size()) eval();
    cout << num.top() << endl;
    return 0;
}

4. 队列 tt = -1,hh = 0;

1. 模拟队列

原题链接

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt = -1;

int main()
{
    cin >> m;

    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            q[ ++ tt] = x;
        }
        else if (op == "pop") hh ++ ;
        else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;
        else cout << q[hh] << endl;
    }

    return 0;
}

5. 单调栈

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

1. 单调栈(4分钟)

原题链接

原题链接

#include <iostream>

using namespace std;

const int N = 100010;

int stk[N], tt;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        /*读进一个数,和栈里的数比较,如果栈里的数违
        规了,那么出栈*/
        while (tt && stk[tt] >= x) tt -- ;
        //如何栈里的数,不满足,那么输出-1
        if (!tt) printf("-1 ");
        //如果满足 那么输出栈
        else printf("%d ", stk[tt]);
        //输入该值,去比较下一个	
        stk[ ++ tt] = x;
    }

    return 0;
}

6. 单调队列

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

1. 滑动窗口例题(10分钟)

原题链接
原题链接

#include<iostream>
#include<deque>

using namespace std;

const int N = 1e6+10;

int a[N];
deque<int> q;

int main()
{
    int n,k;
    cin >> n >> k;
    
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    for(int i = 1; i <= n; i++)
    {
        if(q.size() && q.front()+k==i)q.pop_front();
        while(q.size() && a[q.back()]>=a[i])q.pop_back();
        q.push_back(i);
        if(i>=k)
            cout << a[q.front()] << ' ';
    }
    cout << endl;
    while(q.size())q.pop_back();
    
    for(int i = 1; i <= n; i++)
    {
        if(q.size() && q.front()+k==i)q.pop_front();
        while(q.size() && a[q.back()]<=a[i])q.pop_back();
        q.push_back(i);
        if(i>=k)
            cout << a[q.front()] << ' ';
    }
    return 0;
}

7. KMP

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

1. KMP字符串(10分钟)

原题链接

二刷体会
  1. next数组存储的是,但j不匹配了,j调整到匹配位置,然后j+1和i去比较
  2. 所以j就需要 = i - 1
  3. 当角标从0开始 j 就得=-1,角标从1开始,j = 0
★三刷体会 ne表示算上第一个和最后一个的前缀后缀相等值
#include <iostream>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
int ne[N];
char s[M], p[N];

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;

    for (int i = 2, j = 0; i <= n; i ++ )
    {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;
    }

    for (int i = 1, j = 0; i <= m; i ++ )
    {
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n)
        {
            printf("%d ", i - n);
            j = ne[j];
        }
    }

    return 0;
}

8. Trie树

1. Trie字符串统计(20分钟)

二刷总结

一行只存储一个数据
原题链接

#include<iostream>

using namespace std;

const int N = 1e5+10;

int a[26][N],idx;
int cnt[N];

int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        char ch;
        cin >> ch;
        
        if(ch == 'I')
        {
            string s;
            cin >> s;
            int p = 0;
            for(int i = 0; i < s.size();i++)
            {
                if(a[s[i] - 'a'][p])
                {
                    p = a[s[i] - 'a'][p];
                }
                else
                {
                    a[s[i] - 'a'][p] = ++idx;
                    p = a[s[i] - 'a'][p];
                }
            }
            cnt[p]++;
        }
        else
        {
            string s;
            cin >> s;
            int p = 0;
            for(int i = 0; i < s.size(); i++)
            {
                if(a[s[i]-'a'][p])
                {
                    p = a[s[i]-'a'][p];
                }
                else
                {
                    cout << 0 << endl;
                    break;
                }
                if(i==s.size()-1)
                {
                    cout << cnt[p] << endl;
                }
            }
        }
    }
    
    
    return 0;
}

★2. 最大异或对

原题链接

★二刷总结

正常思路:

  1. 两个循环遍历,时间复杂度(N2
for(int i = 0; i < n; i++)
{
	for(int j = 0; j < n; j++)
	{
		res = max(res,a[i]^a[j]);
	}
}	
  1. 优化:创建一个Trie树,针对一个数,去遍历Trie树
    (怎么想到用Trie树去优化呢?Trie树可以存储组合性质的信息)
#include<iostream>
#include<algorithm>
using namespace std;
int const N=100010,M=31*N;

int n;
int a[N];
int son[M][2],idx;
//M代表一个数字串二进制可以到多长

void insert(int x)
{
    int p=0;  //根节点
    for(int i=30;i>=0;i--)
    {
        int u=x>>i&1;   /取X的第i位的二进制数是什么  x>>k&1(前面的模板)
        if(!son[p][u]) son[p][u]=++idx; ///如果插入中发现没有该子节点,开出这条路
        p=son[p][u]; //指针指向下一层
    }
}
int search(int x)
{
    int p=0;int res=0;
    for(int i=30;i>=0;i--)
    {                               
    ///从最大位开始找
        int u=x>>i&1;
        if(son[p][!u]) 如果当前层有对应的不相同的数
        {   ///p指针就指到不同数的地址

          p=son[p][!u];
          res=res*2+1;
             ///*2相当左移一位  然后如果找到对应位上不同的数res+1 例如    001
        }                                                       ///       010 
        else                                                      --->011                                                                           //刚开始找0的时候是一样的所以+0    到了0和1的时候原来0右移一位,判断当前位是同还是异,同+0,异+1
        {
            p=son[p][u];
            res=res*2+0;
        }
    }
    return res;
}
int main(void)
{
    cin.tie(0);
    cin>>n;
    idx=0;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        insert(a[i]);
    }
    int res=0;
    for(int i=0;i<n;i++)
    {   
        res=max(res,search(a[i]));  ///search(a[i])查找的是a[i]值的最大与或值
    }
    cout<<res<<endl;
}

9. 并查集 find merge

1. 合并集合(5分钟)

原题链接
原题链接

#include<iostream>

using namespace std;

const int N=100010;
int p[N];//定义多个集合

int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    /*
    经上述可以发现,每个集合中只有祖宗节点的p[x]值等于他自己,即:
    p[x]=x;
    */
    return p[x];
    //找到了便返回祖宗节点的值
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i]=i;
    while(m--)
    {
        char op[2];
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        if(*op=='M') p[find(a)]=find(b);//集合合并操作
        else
        if(find(a)==find(b))
        //如果祖宗节点一样,就输出yes
        printf("Yes\n");
        else
        printf("No\n");
    }
    return 0;
}

2. 连通块中点的数量(每个集合有多少个元素)

原题链接

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int p[N], cnt[N];

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        cnt[i] = 1;
    }

    while (m -- )
    {
        string op;
        int a, b;
        cin >> op;

        if (op == "C")
        {
            cin >> a >> b;
            a = find(a), b = find(b);
            if (a != b)
            {
                p[a] = b;
                cnt[b] += cnt[a];
            }
        }
        else if (op == "Q1")
        {
            cin >> a >> b;
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else
        {
            cin >> a;
            cout << cnt[find(a)] << endl;
        }
    }

    return 0;
}

★3. 食物链

原题链接

法一: x,x+n,x+n+n merge(f[x+n],f[x])

思路:

  1. 因为有三种物种,A吃B,B吃C,C吃A
  2. 如果我们用一个数组存储,那么比如1吃2,那么我们让2的角标处的值标记成1,如果3吃2,那怎么标记?一个数组指定标记不过来。
  3. 那么我们想用三个数组存储,其实也存储不过来,因为角标就那么几个,
  4. 最好的方法就是,用x,x+n,x+n+n来表示
    比如1吃2,那么就可能有三种情况,
    A类中的1吃B类的2 : fa[1] = fa[2+n+n]
    B类中的1吃C类的2 : fa[1+n] = fa[2]
    C类中的1中A类的2 : fa[1+n+n] = fa[2+n];
    这样的话,就会有3*n个角标,就可以充分表达
    A中的1吃B中的2(B中的2用2+n表示)
    这样的话就不会出现数字冲突

A吃B
则让f[A] = B

二刷总结

a吃b
f[a+n] = f[b]
或者
f[b+n] = f[a]

/*

*/

#include <bits/stdc++.h>
using namespace std;
int fa[200000];
int n,m,k,x,y,ans;
int get(int x)
{
    if(x==fa[x]) 
        return x;
    return fa[x]=get(fa[x]);
}
void merge(int x,int y)
{
    fa[get(x)]=get(y);
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=3*n;i++) 
        fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&k,&x,&y);
        if(x>n || y>n) 
            ans++;
        else if(k==1)
        {
            if(get(x)==get(y+n) || get(x)==get(y+n+n)) //如果x,y是同类,但是x是y的捕食中的动物,或者x是y天敌中的动物,那么错误.
                ans++;
            else
            {
                merge(x,y);
                merge(x+n,y+n);
                merge(x+n+n,y+n+n);
            }
        }
        else
        {
            if(x==y || get(x)==get(y) || get(x)==get(y+n)) //x就是y,或者他们是同类,再或者是y的同类中有x
                ans++;//都是假话
            else
            {
                merge(x,y+n+n);//y的捕食域加入x
                merge(x+n,y);//x的天敌域加入y
                merge(x+n+n,y+n);//x的捕食域是y的同类域.
            }
        }
    }
    cout<<ans<<endl;
}
法二:将有关系的都存储在一个部落,用到根节点的距离表示关系
二刷总结
  1. X吃Y 让x的祖宗等于y的祖宗 或者 y的祖宗等于x的祖宗都可以
  2. find查找函数中,具有压缩路径的作用,所以在写的时候,先找到此根节点,然后依次压缩,如下代码
int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

不可以如下代码

int find(int x)
{
    if (p[x] != x)
    {
        d[x] += d[p[x]];
        return p[x] = find(p[x]);
    }
    return p[x];
}

  1. 在查询合并过程中,比如查询x的父节点,应该用一个变量记录下来,不能多次find,不然找不到x的原来父节点了
  2. 初始化 p[i] = i; d[i] = 0;
  3. 合并的时候,画图即可明白彼此的距离

具体过程如下:

#include <iostream>

using namespace std;

const int N = 50010;

int n, m;
int p[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    int res = 0;
    while (m -- )
    {
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);

        if (x > n || y > n) res ++ ;
        else
        {
            int px = find(x), py = find(y);
            if (t == 1)
            {
                if (px == py && (d[x] - d[y]) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] - d[x];
                }
            }
            else
            {
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }

    printf("%d\n", res);

    return 0;
}

10. 堆 ✔ ✔

模板

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
  1. 向下调整
    法一:如果孩子节点 小于当前节点 那么交换 然后递归
void down(int u)
{
    int t = u;
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

法二:parent = child * 2 直到 超出范围

    public void createHeap() {
        for (int parent = (usedSize-1-1) / 2; parent >= 0 ; parent--) {
            shiftDown(parent,usedSize);
        }
    }

    /**
     * 实现 向下调整
     * @param parent 每棵子树的根节点的下标
     * @param len 每棵子树的结束位置
     */

    private void shiftDown(int parent ,int len) {
        int child = 2 * parent + 1;
        //最起码是有左孩子
        while (child < len) {
            //判断 左孩子 和 右孩子  谁最大,前提是  必须有  右孩子
            if(child+1 < len && elem[child] < elem[child+1]) {
                child++;//此时 保存了最大值的下标
            }
            if(elem[child] > elem[parent]) {
                swap(elem,child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

    private void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

堆的操作

  1. 删除最值
  1. 因为最小值,位于数组的最前端,所以我们可以让最前端等于最后端的值,(此时堆的最小值已经没了)
  2. 但是呢,堆里出现了两个最前端的值,所以需要再–sz删除最后段的值(不要怕,最后段的值,已经保存到最前端的结点上,)
  3. 最后把最顶端的点 用down
  1. 修改值(改,先down,后up)

在这里插入图片描述

在这里插入图片描述

孩子和父亲的关系
在这里插入图片描述

数组建堆
在这里插入图片描述

1. 堆排序 ✔ ✔

原题链接

在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int h[N], cnt;

void down(int u)
{
    int t = u;
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    cnt = n;

    for (int i = n / 2; i; i -- ) down(i);

    while (m -- )
    {
        printf("%d ", h[1]);
        h[1] = h[cnt -- ];
        down(1);
    }

    puts("");
    return 0;
}

2. 模拟堆 ✔ ✔

原题链接

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <string.h>

using namespace std;

const int N = 100010;

int h[N], ph[N], hp[N], cnt;

void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

int main()
{
    int n, m = 0;
    scanf("%d", &n);
    while (n -- )
    {
        char op[5];
        int k, x;
        scanf("%s", op);
        if (!strcmp(op, "I"))
        {
            scanf("%d", &x);
            cnt ++ ;
            m ++ ;
            ph[m] = cnt, hp[cnt] = m;
            h[cnt] = x;
            up(cnt);
        }
        else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
        else if (!strcmp(op, "DM"))
        {
            heap_swap(1, cnt);
            cnt -- ;
            down(1);
        }
        else if (!strcmp(op, "D"))
        {
            scanf("%d", &k);
            k = ph[k];
            heap_swap(k, cnt);
            cnt -- ;
            up(k);
            down(k);
        }
        else
        {
            scanf("%d%d", &k, &x);
            k = ph[k];
            h[k] = x;
            up(k);
            down(k);
        }
    }

    return 0;
}

11. 哈希表 ✔ ✔

模板

(1) 拉链法
    int h[N], e[N], ne[N], idx;

    // 向哈希表中插入一个数
    void insert(int x)
    {
        int k = (x % N + N) % N;
        e[idx] = x;
        ne[idx] = h[k];
        h[k] = idx ++ ;
    }

    // 在哈希表中查询某个数是否存在
    bool find(int x)
    {
        int k = (x % N + N) % N;
        for (int i = h[k]; i != -1; i = ne[i])
            if (e[i] == x)
                return true;
 
        return false;
    }

(2) 开放寻址法
    int h[N];

    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    int find(int x)
    {
        int t = (x % N + N) % N;
        while (h[t] != null && h[t] != x)
        {
            t ++ ;
            if (t == N) t = 0;
        }
        return t;
    }

例1. 模拟散列表 ✔ ✔

原题链接

在这里插入图片描述

拉链法代码(链表)
  1. N取大于范围的第一个质数
  2. k = (x % N + N) % N; (%N 为了避免超级大的值 + N 为了避免出现负数 再%N为了正数+N超出范围)
  3. memset(h, -1, sizeof h);
#include <cstring>
#include <iostream>

using namespace std;

const int N = 1e5 + 3;  // 取大于1e5的第一个质数,取质数冲突的概率最小 可以百度

//* 开一个槽 h
int h[N], e[N], ne[N], idx;  //邻接表

void insert(int x) {
    // c++中如果是负数 那他取模也是负的 所以 加N 再 %N 就一定是一个正数
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x) {
    //用上面同样的 Hash函数 讲x映射到 从 0-1e5 之间的数
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i]) {
        if (e[i] == x) {
            return true;
        }
    }
    return false;
}

int n;

int main() {
    cin >> n;

    memset(h, -1, sizeof h);  //将槽先清空 空指针一般用 -1 来表示

    while (n--) {
        string op;
        int x;
        cin >> op >> x;
        if (op == "I") {
            insert(x);
        } else {
            if (find(x)) {
                puts("Yes");
            } else {
                puts("No");
            }
        }
    }
    return 0;
}
开放寻址法代码(有空位就存,空位用null=0x3f3f3f3f)
  1. 大于2倍的第一个质数
#include <cstring>
#include <iostream>

using namespace std;

//开放寻址法一般开 数据范围的 2~3倍, 这样大概率就没有冲突了
const int N = 2e5 + 3;        //大于数据范围的第一个质数
const int null = 0x3f3f3f3f;  //规定空指针为 null 0x3f3f3f3f

int h[N];

int find(int x) {
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x) {
        t++;
        if (t == N) {
            t = 0;
        }
    }
    return t;  //如果这个位置是空的, 则返回的是他应该存储的位置
}

int n;

int main() {
    cin >> n;

    memset(h, 0x3f, sizeof h);  //规定空指针为 0x3f3f3f3f

    while (n--) {
        string op;
        int x;
        cin >> op >> x;
        if (op == "I") {
            h[find(x)] = x;
        } else {
            if (h[find(x)] == null) {
                puts("No");
            } else {
                puts("Yes");
            }
        }
    }
    return 0;
}

★例2. 字符串哈希 ✔ ✔

原题链接

二刷思路
  1. 把一段字符串转成一段数字,便可以直接比较相等
  2. 怎么转?进制大于26
  3. 样例没找全 p[r-l+1] 这个点没找到
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+5,P = 131;//131 13331
ULL h[N],p[N];

// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或  13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL query(int l,int r){
    return h[r] - h[l-1]*p[r-l+1];
}
int main(){
    int n,m;
    cin>>n>>m;
    string x;
    cin>>x;

    //字符串从1开始编号,h[1]为前一个字符的哈希值
    p[0] = 1;
    h[0] = 0;
    for(int i=0;i<n;i++){
        p[i+1] = p[i]*P;            
        h[i+1] = h[i]*P +x[i];      //前缀和求整个字符串的哈希值
    }

    while(m--){
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(query(l1,r1) == query(l2,r2)) printf("Yes\n");
        else printf("No\n");

    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/197277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机网络】Linux下路由配置总结

文章目录路由的基础知识Linux内核路由表使用route -n命令查看Linux内核路由表三种路由类型说明(Flags)配置路由route的命令设置包转发静态路由配置参考路由的基础知识 1&#xff09;路由概念 路由&#xff1a; 跨越从源主机到目标主机的一个互联网络来转发数据包的过程路由器…

多线程代码案例之单例模式

目录 单例模式 饿汉模式 懒汉模式 问题一 问题二 问题三 单例模式 单例模式&#xff0c;是设计模式的一种。在有些特定场景中&#xff0c;有的特定的类&#xff0c;只能创建出一个实例&#xff0c;不应该创建多个实例。单例模式就可以保证这样的需求。例如JDBC中的Data…

OpenMMLab AI实战营笔记前两次课

文章目录1计算机视觉算法基础与 OpenMMLabCV引入OpenMMLab基础知识&#xff1a;2 计算机视觉之图像分类算法基础传统方法--设计图像特征AlexNet VGG 等神经网络搜索&#xff08;2016&#xff09;Vision/Swin Transformer轻量化卷积神经网络注意力机制 Attention Mechanism模型学…

文档存储Elasticsearch系列--3分布式存储和搜索过程

前言&#xff1a;ES 作为分布式文档的存储&#xff0c;它的存储过程是怎样的&#xff0c;它的分布式检索过程又是怎样的&#xff1b; 1 分布式存储过程&#xff1a; 为了说明目的, 我们 假设有一个集群由三个节点组成。 它包含一个叫 blogs 的索引&#xff0c;有两个主分片&a…

linux查看/设置某个进程运行的CPU核

目录 1.ps -eF 2.top命令 3.pidstat命令 4.使用taskset指令 5.使用taskset指定进程运行在CPU核 1.ps -eF #查看fwd进程运行在哪个cpu核上 [rootCENTOS57 rpm]# ps -eF | grep fwd 2.top命令 (1)top (2)按f键可以选择下面配置选项 P Last Used Cpu (SMP) (3)Es…

【深度学习】YOLO系列(v1-v3+tinyv3)解析

YOLOv1 正负样本选取 如果目标的中心落在cell中,那么这个cell就负责预测这个类别。 由于每个cell预测两个bbox,那么选择与GT IOU大的bbox来预测这个目标,也就是这一个框的 1 i j o b j = 1 , 1 i j n o b j

通过Python的pptx库操作ppt-替换文本和图片-批量生成任意自定义图片

通过Python的pptx库操作ppt-替换文本和图片-批量生成任意自定义图片 作者&#xff1a;虚坏叔叔 博客&#xff1a;https://xuhss.com 早餐店不会开到晚上&#xff0c;想吃的人早就来了&#xff01;&#x1f604; 一、前言 这是一个全部的脚本&#xff0c;我们知道&#xff0c;…

阿里巴巴最全Java、架构师、大数据、算法PPT技术栈图册

我只截图不说话&#xff0c;PPT大全&#xff0c;氛围研发篇、算法篇、大数据、Java后端架构&#xff01;除了大家熟悉的交易、支付场景外&#xff0c;支撑起阿里双十一交易1682亿元的“超级工程”其实包括以下但不限于客服、搜索、推荐、广告、库存、物流、云计算等。 Java核心…

第二章 Linux系统安装

第一节 安装计划 基本思路是使用VMWare这样的虚拟机软件创建一个“虚拟计算机”&#xff0c;在虚拟机上安装Linux系统。 安装vm软件通过vm软件来创建一个虚拟机空间通过vm软件来在创建好的虚拟空间上&#xff0c;安装我们的Centos操作系统使用Centos 第二节 vmware下载安装 和…

python-实现保留3位有效数字(四舍六入五成双规则)

项目场景&#xff1a; 实现保留3位有效数字&#xff08;四舍六入五成双规则&#xff09; 问题描述 输入&#xff1a;输出&#xff1a; 1234 123412 12.04 4.000.2 0.2000.32 0.3201.3 1.301.235 1.241.245 1.241.2451 1.25示例分析&#xff1a; 解决代码&#xff1a; from de…

jvm启动流程以及自定义加载器

类加载运行过程&#xff0c;当我们用java命令运行某个类的main函数启动程序时&#xff0c;首先需要通过类加载器把主类加载到JVM。public class Math {public static final int initData 666;public static final User user new User();public int compute() {// 一个方法对应…

【C++】对象与类

【C】对象与类 文章目录【C】对象与类1、定义1.1 对象的定义1.2 类的定义2、对象与类的创建2.1 类的创建2.2 对象的创建3、封装3.1 访问限定符3.2 对封装的解释4、类的实例化5、类、对象大小6、this指针6.1 this指针概念6.2 this指针特点1、定义 1.1 对象的定义 现实世界对对…

写哪个IB科目的EE最易得A?

综合了IB论坛上学生的建议&#xff0c;根据IB毕业生们的看法&#xff1a; E同学&#xff1a;选择你感兴趣的科目写EE。 R同学&#xff1a;我推荐写IB英语EE&#xff0c;在我看来&#xff0c;英语很容易找到你感兴趣的内容&#xff0c;因为英语 EE 适用于诸多的不同主题。我觉得…

录制PPT课件哪个录屏软件好?这3款值得收藏

使用PPT课件进行教学&#xff0c;不仅可以让抽象的知识具体化&#xff0c;还可以让课堂变得更加生动有趣。在制作视频课程时&#xff0c;需要对PPT课件进行录制&#xff0c;那你知道录制PPT课件哪个录屏软件好吗&#xff1f;今天小编就给大家介绍3款值得收藏的录屏软件&#xf…

为什么GIF文件是图像文件而不是视频文件?(GIF文件格式详解)

今天整理硬盘的时候想到一个问题&#xff1a;GIF 是放在静态图像文件里还是视频文件里&#xff1f; 因为放视频里吧&#xff0c;GIF 的分类是静态图像&#xff0c;而且由图像组成&#xff1b;放图像里吧&#xff0c;它又会动。 然后我就开始想&#xff1a;GIF 会动但为什么被归…

分享113个图片切换JS特效,总有一款适合您

分享113个图片切换JS特效&#xff0c;总有一款适合您 113个图片切换JS特效下载链接&#xff1a;https://pan.baidu.com/s/1NNSP-DMf3n0PeNbdNd8jEg?pwdsfwr 提取码&#xff1a;sfwr Python采集代码下载链接&#xff1a;https://wwgn.lanzoul.com/iKGwb0kye3wj jQuery中间…

Java基础学习笔记(十九)—— 多线程

多线程1 多线程相关概念2 多线程的实现方式2.1 继承Thread类2.2 实现Runnable接口2.3 实现Callable接口3 线程休眠4 线程优先级5 守护线程6 线程同步6.1 案例引入6.2 同步代码块6.3 同步方法6.4 Lock锁6.5 死锁1 多线程相关概念 并行与并发&#xff1a; 并行&#xff1a;在同…

未授权和敏感文件泄露

目前存在未授权访问漏洞的服务主要 包括&#xff1a;NFS、Samba、LDAP、Rsync、FTP、GitLab、Jenkins、 MongoDB、Redis、ZooKeeper、ElasticSearch、Memcache、CouchDB、 Docker、Solr、Hadoop等。 redis未授权 通过手工进行未授权访问验证&#xff0c;在安装Redis服务的Kal…

MATLAB 线性整数规划

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

oracle官网下载指定版本的jdk

文章目录前言下载JDK环境变量前言 oracle官网下载jdk&#xff0c;如果是想下载指定版本的&#xff0c;总是提示需要你登录。如何解决呢&#xff1f; 下载JDK oracle官网 https://www.oracle.com 或者直达此页 https://www.oracle.com/java/technologies/downloads/archive…