NEUQ-ACM2022预备队 2023春 第五周

第五周

第一题 碰撞2

题目链接

题目1

x y xy xy 坐标系中有 N N N 个人,第 i i i 个人的位置是 ( X i , Y i ) (X_i,Y_i) (Xi,Yi) ,并且每个人的位置都不同。
​有一个由 L L L R R R 组成的长为 N N N 的字符串 S S S S i = R S_i = R Si=R 代表第 i i i 个人面向右, S i = L S_i = L Si=L 代表第 i i i 个人面向左。
​现在所有人开始朝着他们各自面向的方向走,即面向右 x x x 就增,面向左 x x x 就减。
把两个人对向行走到一个位置称为一次碰撞。请问如果人们可以无限走下去,会有人产生碰撞吗?

思路1

只有 y y y 相同的人才有可能相撞。
对于 y y y 相同的一些人,只有两种情况不会发生碰撞:

  1. 全部朝一个方向
  2. 左边一部分人朝左,右边一部分人朝右

所以只需要从左向右判断这些人是不是先朝左然后再朝右即可。
由于坐标的范围很大,不可能对每一个 y y y 都创建一个对象,所以可以用map来记录。

代码1

#include<iostream>
#include<vector>
#include<algorithm>
#include<map>
using namespace std;

constexpr int MAXN = 2e5 + 5;
//map,键为y左边,值为所有人的x坐标和朝向
map<int, vector<pair<int, char> > > m;
int cx[MAXN], cy[MAXN];

//判断是否碰撞
bool judge()
{
    //遍历每一个y值
    for (pair<int, vector<pair<int, char> > > p : m)
    {
        //对所有人的位置排序,保证站在左边的人在前面
        sort(p.second.begin(), p.second.end());
        //记录当前队列里面的人是朝左还是朝右
        //先按照朝左来判断。如果出现了朝右的,后面所有人都必须朝右
        bool left = true;
        //前一个人的位置,防止出现开始时就站在一起的情况
        int last = -1;
        for (int i = 0; i < p.second.size(); i++)
        {
            //判断有没有人开始就站在一起
            if (last == p.second[i].first)
            {
                return true;
            }
            last = p.second[i].first;
            //还是按照朝左判断时
            if (left)
            {
                //遇到了朝右的
                if (p.second[i].second != 'L')
                {
                    //更新标记
                    left = false;
                }
            }
            //按照朝右判断时
            else
            {
                //这时还有朝左的肯定会碰撞
                if (p.second[i].second != 'R')
                {
                    return true;
                }
            }
        }
    }
    //遍历完没有问题就不会碰撞
    return false;
}

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> cx[i] >> cy[i];
    }
    string s;
    cin >> s;
    for (int i = 1; i <= n; i++)
    {
        m[cy[i]].push_back({ cx[i], s[i - 1] });
    }
    cout << (judge() ? "Yes" : "No") << endl;
    
    return 0;
}

第二题 优美!最长上升子序列

题目链接

题目2

给定一个数组。派派希望从中选择最长的递增的子序列,此外还要满足后一个下标能整除前一个下标。

思路2

还是通过动态规划来解决。
状态:以 i i i 结尾的最长的递增的子序列
状态转移方程: f ( i ) = m a x ( f ( j ) + 1 ) ,   i m o d    j = 0 ,   a i > a j f(i) = max(f(j) + 1), \space i \mod j = 0, \space a_i > a_j f(i)=max(f(j)+1), imodj=0, ai>aj
起始条件: f ( i ) = 1 , i ∈ [ 1 , n ] f(i) = 1, i \in [1, n] f(i)=1,i[1,n]

由于题目要求下标整除,所以可以用前面的下标去推后面所有能整除的下标,这样更加快速。

代码2

#include<iostream>
#define int long long
using namespace std;

constexpr int MAXN = 1e6 + 5;
int a[MAXN];
//dp结果
int ans[MAXN];

signed main()
{
    int t;
    cin >> t;
    //用于输出格式
    bool first = true;
    while (t--)
    {
        int n;
        //当前询问的答案
        int tans = 1;
        cin >> n;
        for (int i = 1; i <= n; i++)
        {
            cin >> a[i];
            ans[i] = 1;
        }
        //dp
        for (int i = 1; i <= n; i++)
        {
            //寻找所有能整除i的j
            for (int j = i * 2; j <= n; j += i)
            {
                //转移方程
                if (a[j] > a[i])
                {
                    ans[j] = max(ans[j], ans[i] + 1);
                    //更新答案
                    tans = max(ans[j], tans);
                }
            }
        }
        //输出
        if (first)
        {
            cout << tans;
            first = false;
        }
        else
        {
            cout << ' ' << tans;
        }
    }
    return 0;
}

第三题 巨大的牛棚

题目链接

题目3

农夫约翰想要在他的正方形农场上建造一座正方形大牛棚。找一个能够让他在空旷无树的地方修建牛棚的地方。他的农场划分成 n × n n \times n n×n 的方格。计算并输出,在他的农场中,不需要砍树却能够修建的最大正方形牛棚。

思路3

因为农场较大,枚举会比较费时。所以可以考虑使用二分答案。
将边长进行二分,再枚举每一个位置进行判断即可。
此外计算一块地上数的个数也很费时。而使用矩阵前缀和可以快速得到结果。

矩阵前缀和计算方法: s u m ( i , j ) = s u m ( i − 1 , j ) + s u m ( i , j − 1 ) − s u m ( i − 1 , j − 1 ) + a ( i , j ) sum(i, j) = sum(i - 1, j) + sum(i, j - 1) - sum(i - 1, j - 1) + a(i, j) sum(i,j)=sum(i1,j)+sum(i,j1)sum(i1,j1)+a(i,j)
通过前缀和得到一块区域和的方法: p s u m ( x , y , l ) = s u m ( x + l , y + l ) − s u m ( x + l , y ) − ( x , y + l ) + s u m ( x , y ) psum(x, y, l) = sum(x + l, y + l) - sum(x + l, y) - (x, y + l) + sum(x, y) psum(x,y,l)=sum(x+l,y+l)sum(x+l,y)(x,y+l)+sum(x,y)

代码3

#include<iostream>
using namespace std;

constexpr int MAXN = 1e3 + 5;
bool a[MAXN][MAXN];
//前缀和
int sum[MAXN][MAXN];

//判断答案
bool judge(int n, int l)
{
    for (int i = 1; i + l <= n; i++)
    {
        for (int j = 1; j + l <= n; j++)
        {
            if (sum[i + l][j + l] + sum[i][j] - sum[i][j + l] - sum[i + l][j] == 0)
            {
                return true;
            }
        }
    }
    return false;
}

int main()
{
    int n, t;
    cin >> n >> t;
    for (int i = 1; i <= t; i++)
    {
        int x, y;
        cin >> x >> y;
        a[x][y] = true;
    }
    //计算前缀和
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
        }
    }
    //二分
    int l = 0, r = n, ans = 0;
    while (l <= r)
    {
        int mid = (l + r) / 2;
        if (judge(n, mid))
        {
            l = mid + 1;
            //更新答案
            ans = mid;
        }
        else
        {
            r = mid - 1;
        }
    }
    cout << ans << endl;
    return 0;
}

第四题 高利贷

题目链接

题目4

贷款机构利率按月累计,贷款人在贷款期间每月偿还固定的分期付款金额。
给出贷款的原值为 n n n,分期付款金额 m m m 和分期付款还清贷款所需的总月数 k k k,求该贷款的月利率 p p p

思路4

这道题无法直接解出答案。但是题目只要求误差小于 1 × 1 0 − 6 1 \times 10^{-6} 1×106,所以可以通过二分来计算答案。
使用二分的答案,计算按答案得到的原款数,再进行比较。
公式为 n = ∑ i = 1 k m ( 1 + p ) i n = \sum_{i = 1}^k\frac m {(1 + p)^i} n=i=1k(1+p)im

代码4

#include<iostream>
#include<iomanip>
using namespace std;

//误差
constexpr double MISS = 1e-7;

//判断二分答案
bool judge(double n, double m, int k, double p)
{
    //每月利率的倍数
    double ratio = 1 + p;
    //当前月利率
    double pk = 1;
    //计算得到的原款
    double sum = 0;
    //循环计算每月原款相当于原款数
    for (int i = 1; i <= k; i++)
    {
        pk *= ratio;
        sum += m / pk;
    }
    return sum >= n;
}

int main()
{
    double n, m, ans;
    int k;
    cin >> n >> m >> k;
    //二分
    double l = 0, r = 5;
    while (r - l >= MISS)
    {
        double mid = (l + r) / 2;
        if (judge(n, m, k, mid))
        {
            l = mid;
            ans = mid;
        }
        else
        {
            r = mid;
        }
    }
    cout << setiosflags(ios::fixed) << setprecision(6) << ans << endl;
    return 0;
}

第五题 背包

题目链接

题目5

有一个背包,背包的体积为 w w w,有 n n n 个物品,每一个物品的体积为 a i a_i ai。cc希望将其中的一些物品放入他的背包中,他希望这些物品的体积之和至少是背包体积的一半,并且不超过背包的体积,即 ⌈ w 2 ⌉ ≤ s u m ≤ w \lceil \frac w2 \rceil \leq sum \leq w 2wsumw
判断这些物品中有没有符合条件的物品组合,如果有输出"YES", 没有输出"NO"。

思路5

遍历每一件物品。如果有物品大于体积的一半,则肯定可以。有物品小于体积的一半,并且现在背包还没到一半,那么放进去肯定放得下,就直接放进去;如果此时超过了一半则同样可以。

代码5

#include<iostream>
using namespace std;

constexpr int MAXN = 2e5 + 5;

int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        int n;
        //now是当前背包内的东西
        long long w, now = 0;
        cin >> n >> w;
        //一半的大小,向上取整
        long long half = (w + 1) / 2;
        //标记是否能超过一半
        bool b = false;
        for (int i = 1; i <= n; i++)
        {
            //读入每一个物品
            int temp;
            cin >> temp;
            //有物品超过一半就一直跳过
            if (b)
            {
                continue;
            }
            //物品超过容量
            if (temp > w)
            {
                continue;
            }
            //物品超过一半
            if (temp >= half)
            {
                b = true;
            }
            //物品小于一半,直接放包里
            else
            {
                now += temp;
                //判断是否超过一半
                if (now >= half)
                {
                    b = true;
                }
            }
        }
        cout << (b ? "YES" : "NO") << endl;
    }
    return 0;
}

第六题 三回文序列

题目链接

题目6

三回文序列是形如 a … a ⏟ k 1 b … b ⏟ k 2 a … a ⏟ k 1 \underbrace{a \dots a}_{k_1}\underbrace{b \dots b}_{k_2}\underbrace{a \dots a}_{k_1} k1 aak2 bbk1 aa 的序列。
找到序列中最长的三回文的子序列的长度。

思路6

遍历 a a a k 1 k_1 k1 可能的所有取值,然后从序列两端开始分别找 k 1 k_1 k1 a a a,然后再找剩下的部分中出现次数最多的 b b b
找出现次数最多依然可以使用前缀和来加快运算速度。

代码6

#include<iostream>
#include<cstring>
using namespace std;

constexpr int MAXN = 2e5 + 5, MAXA = 26;
int nums[MAXN];
//前缀和
int sums[MAXN][MAXA + 1];

int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        int n, ans = 0;
        cin >> n;
        //清空前缀和
        memset(sums, 0,sizeof(sums));
        for (int i = 1; i <= n; i++)
        {
            cin >> nums[i];
        }
        //计算前缀和
        for (int i = 1; i <= n; i++)
        {
            //遍历b的取值 1 - 26
            for (int j = 1; j <= MAXA; j++)
            {
                if (nums[i] == j)
                {
                    sums[i][j] = sums[i - 1][j] + 1;
                }
                else
                {
                    sums[i][j] = sums[i - 1][j];
                }
            }
        }
        //遍历a的取值 1 - 26
        for (int i = 1; i <= MAXA; i++)
        {
            //左侧指针,右侧指针,左侧a的次数,右侧a的次数
            int pl = 1, pr = n, cntl = 0, cntr = 0;
            //遍历k1 0 - n
            for (int j = 0; j <= n; j++)
            {
                //移动左指针,找到j个a
                while (pl < pr && cntl < j)
                {
                    if (nums[pl] == i)
                    {
                        cntl++;
                    }
                    pl++;
                }
                //移动右指针,找到j个a
                while (pl < pr && cntr < j)
                {
                    if (nums[pr] == i)
                    {
                        cntr++;
                    }
                    pr--;
                }
                //找不到指定个数的a(j太大了)
                if (cntl != j || cntr != j)
                {
                    break;
                }
                //去找b的值
                for (int k = 1; k <= MAXA; k++)
                {
                    //更新答案,当前答案为j的个数乘以2(两边)加上中间出现的次数
                    ans = max(ans, 2 * j + sums[pr][k] - sums[pl - 1][k]);
                }
            }
        }

        cout << ans << endl;
    }
    return 0;
}

第七题 简单的异或问题

题目链接

题目7

有一组整数 { 0 , 1 , 2 , … , 2 m − 1 } \lbrace 0, 1, 2, \dots , 2^m − 1 \rbrace {0,1,2,,2m1},请从中选出 k k k 个数,使得这 k k k 个数的异或和为 n n n, 请输出最大的满足条件的 k k k

思路7

m > 1 m > 1 m>1 时, 0 0 0 2 m − 1 2^m - 1 2m1 所有数异或得到的都是 0 0 0。所有的数可以组成 ( 0 , 2 m − 1 ) , ( 1 , 2 m − 2 ) , … (0, 2^m - 1), (1, 2^m - 2), \dots (0,2m1),(1,2m2),的偶数组数,而每一组数的异或值都是 2 m − 1 2^m - 1 2m1,异或和也就是 0 0 0
可以看出,每组数异或得到 2 m − 1 2^m - 1 2m1,所以用 2 m − 1 2^m - 1 2m1 和与 n n n 同组的数异或得到的数就是 n n n。而剩下的所有组有偶数个( 2 m − 1 2 ^ m - 1 2m1 n n n 的组不包括),异或和为 0 0 0,所以剩下的组都可以选择。最好的情况就是选了剩下组、 0 0 0 2 m − 1 2^m - 1 2m1 和与 n n n 同组的数,一共 2 m − 1 2 ^ m - 1 2m1 个数。
但是 m = 1 m = 1 m=1时不同,如果 n = 0 n = 0 n=0,则只能选 0 0 0;如果 n = 1 n = 1 n=1,则只能选 0 0 0 1 1 1 1 1 1
此外还有两个特殊情况。 n = 0 n = 0 n=0,此时可以把所有数都选上。 n = 2 m − 1 n = 2 ^ m - 1 n=2m1,此时只不选 2 m − 1 2 ^ m - 1 2m1,也是 2 m − 1 2 ^ m - 1 2m1 个。

代码7

#include<iostream>
using namespace std;

int main()
{
    long long n, m;
    cin >> n >> m;
    long long maxn = (1ll << m) - 1;
    //n = 0, m > 1
    if (n == 0 && m != 1)
    {
        cout << maxn + 1 << endl;
    }
    //m = 1
    else if (m == 1)
    {
        if (n == 0)
        {
            cout << 1 << endl;
        }
        else
        {
            cout << 2 << endl;
        }
    }
    //2 ^ m - 1的情况
    else
    {
        cout << maxn << endl;
    }
    return 0;
}

第八题 子串的循环挪动

题目链接

题目8

给出一个字符串 s s s,你需要执行 m m m 个任务。每个任务给出两个下标 l i l_i li r i r_i ri 和一个整数 k i k_i ki(字符串的下标从 1 1 1 开始),表示你需要循环挪动 s s s 的子串 s [ l i … r i ] s[l_i \dots r_i] s[liri] k i k_i ki 次。请从前到后依次执行给出的每个任务。
字符串的循环挪动操作:将最后一个字符移到第一个字符的位置,并且将其他所有字符向右移一个位置。

思路8

每次任务会将一个字符串分成四份:

  1. l i l_i li 之前的部分
  2. l i l_i li 到 子串中第 k k k 个之前的部分
  3. 子串中第 k k k 个到 l r l_r lr 的部分
  4. l r l_r lr 之后的部分

执行已至此之后字符串会重组成 1-3-2-4 的形式。
此外 1 ≤ k i ≤ 1000000 1 \leq k_i \leq 1000000 1ki1000000 k k k可能超过子串长度,需要先取模。

代码8

#include<iostream>
using namespace std;

int main()
{
    string s;
    cin >> s;
    int m;
    cin >> m;
    while (m--)
    {
        int l, r, k;
        cin >> l >> r >> k;
        //取模
        k %= (r - l + 1);
        if (k == 0)
        {
            continue;
        }
        //重组
        s = s.substr(0, l - 1) + s.substr(r - k, k) + s.substr(l - 1, r - l - k + 1) + s.substr(r, s.size() - r);
    }
    cout << s << endl;
    return 0;
}

第九题 弗拉德和糖果 II

题目链接

题目9

n n n 种糖果,第 i i i 种糖果有 a i a_i ai 个( 1 ≤ i ≤ n 1 \leq i \leq n 1in)。
是否可以在不连续吃两个相同的糖果的情况下吃掉所有的糖果。

思路9

主要问题集中在最多的糖果上,因为最多的糖果最容易连续吃到。
如果最多的糖果要多于其他糖果的和再加一,则不管怎么吃,都会出现连续吃两次的情况。
如果小于等于,则可以保证每次吃一个最多的糖果,都有另一种糖果能吃。

代码9

#include<cstdio>
#include<algorithm>

constexpr int MAXN = 5000000 + 5;
long long a[MAXN];

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", a + i);
    }
    //排序找出最多的糖果
    std::sort(a + 1, a + 1 + n);
    //求和
    long long sum = 0;
    for (int i = 1; i < n; i++)
    {
        sum += a[i];
    }
    //判断条件
    printf(a[n] <= sum + 1 ? "YES" : "NO");
    return 0;
}

第十题 上帝的集合

题目链接

题目10

现在上帝有一个空集合,现在他命令你为他执行下列三种操作 n n n 次,他每次会给你一个操作类型 o p op op

  1. 向集合中插入一个整数 x x x;
  2. 将集合中所有的数加上 x x x;
  3. 输出集合中最小的数,并从集合中将他删除,如果存在多个最小的整数,任意选择一个即可;

思路10

这道题需要保持集合有序,所以可以使用优先队列存储数据。
操作 2 2 2 需要对所有数据进行操作,可能会花费很多时间。可以先存储所有加上的数的和,在出列的时候再加,入列的时候再减。

代码10

#include<vector>
#include<cstdio>
#include<queue>
using namespace std;

int main()
{
    //优先队列,小的数优先
    priority_queue<long long, vector<long long>, greater<long long> > q;
    //o存储一共加了多少
    long long n, o = 0;
    scanf("%lld", &n);
    while (n--)
    {
        int op;
        long long x;
        scanf("%d", &op);
        switch (op)
        {
        case 1:
            scanf("%lld", &x);
            q.push(x - o);
            break;
        case 2:
            scanf("%lld", &x);
            o += x;
            break;
        case 3:
            printf("%lld\n", q.top() + o);
            q.pop();
            break;
        default:
            break;
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值