AtCoder Beginner Contest 439 ABCDEF 题目解析

A - 2^n - 2*n

题意

给定整数 \(N\),输出 \(2^N-2N\)

代码

void solve()
{
    int n;
    cin >> n;
    cout << pow(2, n) - 2 * n;
}

B - Happy Number

题意

一个非负整数是快乐数,当且仅当重复下面的运算一定次数后,这个整数会变成 \(1\)

  • 将整数改为十进制表示法中各个数位平方和
    • 例如,对 \(2026\) 进行一次运算,可以变成 \(2^2+0^2+2^2+6^2 = 4+0+4+36 = 44\)

给定正整数 \(N\),请判断 \(N\) 是否是一个快乐数。

思路

最大的个位数平方为 \(9^2 = 81\),即使给定的是一个全为 \(9\) 的十位数,其数位平方和也只有 \(810\)

因此我们可以模拟“把 \(n\) 改为 \(n\) 的数位平方和”这一过程,并且能够断定过程中遇到的整数最多只有几百的大小。

所以可以开一个略大于 \(2026\)\(n\) 的输入范围)的计数数组 vis 用于标记每个整数是否已经出现过。如果在模拟过程中发现 \(n\) 变成了一个此前已经出现过的整数,说明现在陷入了一个循环,\(n\) 将永远无法变为 \(1\),直接判定为 No 即可。

否则,由于每个数字只会最多被访问一次,在有限次数内一定能变为 \(1\)

代码

bool vis[2030];
// vis[i] 标记 i 是否已经出现过

void solve()
{
    int n;
    cin >> n;
    
    while(true)
    {
        if(n == 1) // 能够变为 1
        {
            cout << "Yes";
            return;
        }
        
        if(vis[n]) // n 这个数字此前已经出现过,说明进入了不包含 1 的循环
        {
            cout << "No";
            return;
        }
        
        vis[n] = true; // 标记 n 已出现过
        
        int s = 0; // 求 n 的数位平方和
        while(n > 0)
        {
            int d = n % 10;
            n /= 10;
            s += d * d;
        }
        n = s; // 把 s 重新交给 n
    }
}

C - 2026

题意

满足以下条件的正整数 \(n\) 被称为好整数

  • 存在一对整数对 \((x,y)\) 满足 \(0 \lt x \lt y\)\(x^2+y^2=n\)

给定正整数 \(N\),请输出所有不超过 \(N\) 的好整数。

思路

\(N \le 10^7\),说明整数对 \((x, y)\) 中两整数均不会超过 \(\sqrt{10^7} \approx 3162\),因此可以考虑直接 \(O(\sqrt{N} ^2)\) 枚举所有可行的整数对 \((x, y)\)

cnt[i] 表示 \(i\) 能被多少种整数对 \((x, y)\) \((0\lt x \lt y)\) 的平方和所表示。每枚举出一对整数对 \((x, y)\) \((0\lt x \lt y)\),则说明 \(x^2 + y^2\) 多了一种表示方法。

最后统计 \(1 \sim N\) 中有多少种整数仅有一种表示方法,输出数量及各个整数。

时间复杂度 \(O(N)\)

代码

int cnt[10000005];
// cnt[i] 表示 i 能被多少种正整数对 x^2+y^2 (x<y) 表示出来

void solve()
{
    int n;
    cin >> n;
    
    for(int i = 1; i * i <= n; i++) // 枚举整数对中第一个整数
        for(int j = i + 1; i * i + j * j <= n; j++) // 枚举整数对中第二个整数
            cnt[i * i + j * j]++;
    
    int ans = 0; // 统计答案数量
    for(int i = 1; i <= n; i++)
        if(cnt[i] == 1) // 只能被唯一表示
            ans++;
    cout << ans << "\n";
    
    for(int i = 1; i <= n; i++)
        if(cnt[i] == 1)
            cout << i << " ";
}

D - Kadomatsu Subsequence

题意

给定一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\dots,A_N)\)

求满足以下所有条件的整数三元组 \((i,j,k)\) 的个数:

  • \(1 \le i,j,k \le N\)
  • \(A_i : A_j : A_k = 7:5:3\)
  • \(j\)\(i, j, k\) 三个整数中最大的数最小的数

思路

因为第三个条件限制 \(A_j\) 一定在最左侧或最右侧,考虑以 \(A_j\) 作为入手点。

由于 \(A_i, A_j, A_k\) 均为整数且呈 \(7:5:3\) 的比值,因此 \(A_i, A_j, A_k\) 一定分别是 \(7, 5, 3\) 的倍数。

枚举序列中每个数字作为 \(A_j\),首先必须保证是 \(5\) 的倍数,然后便可以通过 \(\dfrac{7A_j}{5}\)\(\dfrac{3A_j}{5}\) 求出 \(A_i\)\(A_k\) 的值。

现在先假设 \(j\)\(i, j, k\) 中的最大值,即 \(A_j\) 在三数中出现在最右侧。那么我们只需要能够\(j\) 的左侧任意找出两个分别等于 \(\dfrac{7A_j}{5}\)\(\dfrac{3A_j}{5}\) 的整数,那么这两个整数一定能够与 \(j\) 组成三元组,答案为:

  • \(x\) 表示 \(j\) 的左侧出现 \(\dfrac{7A_j}{5}\) 的次数。
  • \(y\) 表示 \(j\) 的左侧出现 \(\dfrac{3A_j}{5}\) 的次数。
  • \(x\)\(y\) 中各取一个位置,均能与 \(j\) 组成三元组,方案数为 \(x \times y\)

\(j\)\(i, j, k\) 中的最小值同理,只需要在 \(j\) 的右侧用同样的方法计算即可。

由于数值较大,可以采用 map 容器实现指定数值出现次数的统计。

时间复杂度为 \(O(N\log N)\)

代码

int n, a[300005];

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    
    long long ans = 0;
    
    map<int, int> cnt;
    // 从左往右,cnt[x] 记录 i 左侧数字 x 的出现次数
    for(int i = 1; i <= n; i++)
    {
        if(a[i] % 5 == 0)
        {
            int x = cnt[a[i] / 5 * 3];
            int y = cnt[a[i] / 5 * 7];
            ans += 1LL * x * y;
        }
        cnt[a[i]]++;
    }
    
    cnt.clear();
    // 从右往左,cnt[x] 记录 i 右侧数字 x 的出现次数
    for(int i = n; i >= 1; i--)
    {
        if(a[i] % 5 == 0)
        {
            int x = cnt[a[i] / 5 * 3];
            int y = cnt[a[i] / 5 * 7];
            ans += 1LL * x * y;
        }
        cnt[a[i]]++;
    }
    
    cout << ans;
}

E - Kite

题意

\(N\) 个人正在河边放风筝,每个人分别编号为 \(1, 2, \dots, N\)

河流沿着河岸呈直线流淌。

考虑一个二维平面直角坐标系,其中 \(x\) 轴表示河流的方向, \(y\) 轴表示高度。

\(i\) 个人站在 \((A_i, 0)\) 点,他打算把风筝放到 \((B_i, 1)\) 点。

为了避免人与风筝发生碰撞,也为了避免风筝线缠绕在一起,对于 \(i\)\(j\) 两个人来说(\(i \ne j\)),如果满足以下条件,他们就不能同时放风筝:

  • 连接 \((A_i, 0)\)\((B_i, 1)\) 的线段 和 连接 \((A_j, 0)\)\((B_j, 1)\) 的线段 存在一个交点(包括端点)。

请问在遵守上述限制条件的前提下,最多可以有多少个人能同时放风筝?

思路

由于每条线段在高度上的变化均为 \(0 \sim 1\),暂将其忽略,下文用 \((A_i, B_i)\) 描述一条线段的起点与终点的 \(x\) 坐标(与题意含义相同)。

在这种限制下,如果两条线段 \((a, b)\)\((p, q)\) 存在交点,只可能是因为出现了以下四种情况之一:

  • \(a = p\) (人坐标重叠)
  • \(b = q\) (风筝坐标重叠)
  • \(a \lt p\)\(b \gt q\) (靠左的人风筝靠右,下同)
  • \(a \gt p\)\(b \lt q\)

反过来说,如果我们能够选出一些线段 \((a_1, b_1), (a_2, b_2), \dots, (a_k, b_k)\) (已按 \(a_i\) 从小到大排序),并且保证这些线段两两之间不存在交点,那么对于 \(b_i\) 而言一定也呈现出升序的趋势,即:

  • \(a_1 \lt a_2 \lt a_3 \lt \dots \lt a_k\)
  • \(b_1 \lt b_2 \lt b_3 \lt \dots \lt b_k\)

为了保证答案长度最长,我们只需要将输入的所有二元组 \((A_i, B_i)\) 按人的坐标 \(A_i\) 从小到大进行排序,然后对于风筝坐标 \(B_i\) 求一个最长上升子序列即可。

最后,本题中并没有保证人的坐标两两不同,当人的坐标重叠时我们应当只保留其中一个人的风筝。如果排序时对于坐标重叠的人,其风筝也升序排序的话,可能会造成重复选择的情况。因此本题在人的坐标 \(A_i\) 相同时,可以对 \(B_i\) 从大到小排序,以保证同种 \(A_i\) 对最长上升子序列答案的影响最多为 \(1\)

最后的最后,由于 \(N\) 较大, 传统 \(O(N^2)\) 的 LIS 做法不可行,可以考虑优化后的 \(O(N\log N)\) 做法。

代码

struct node
{
    int a, b;
    bool operator < (const node &nd) const
    {
        if(a != nd.a)
            return a < nd.a; // 按人的坐标升序排序
        return b > nd.b; // 按风筝的坐标降序排序,防止在人重合时被选中多条有交点的线段
    }
};

int n;
node r[200005];

int dp[200005], ans = 0;
// dp[i] 表示当前已知的所有长度为 i 的上升子序列中,最后一个数的数值最小是多少
// ans 表示当前最长上升子序列的长度

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> r[i].a >> r[i].b;
    sort(r + 1, r + n + 1);
    
    for(int i = 1; i <= n; i++)
    {
        int x = r[i].b;
        // 按 b 求最长上升子序列
        int p = lower_bound(dp + 1, dp + ans + 1, x) - dp;
        // 获得 dp 数组中第一个 >= x 的位置 p
        if(p <= ans)
        {
            dp[p] = x;
            // 此时长度为 p-1 的上升子序列中,结尾的最小值 < x
            // 便可以在其后面拼上一个 x,使其变为长度为 p 的上升子序列
            // 因此更新长度为 p 的上升子序列的结尾最小值
        }
        else
        {
            dp[++ans] = x;
            // 大于所有数字,可以拼在前面最长的上升子序列后面
            // 形成一个长度为 ans+1 的上升子序列
        }
    }
    cout << ans;
}

F - Beautiful Kadomatsu

题意

当一个长度为 \(k\) 的序列 \(a=(a_1,a_2,\dots,a_k)\) 满足以下条件,将被称作 门松式的

  • \(x\) 表示满足 \(2 \le i \le k-1\)\(a_{i-1} \lt a_i \gt a_{i+1}\) 的整数 \(i\) 数量。
  • \(y\) 表示满足 \(2 \le i \le k-1\)\(a_{i-1} \gt a_i \lt a_{i+1}\) 的整数 \(i\) 数量。
  • 当且仅当 \(x \gt y\) 时,序列 \(a\) 才称作 门松式的

给定一个 \(N\) 的全排列 \(P\),求 \(P\) 中有多少个子序列可以被称作 门松式的,对 \(998\,244\,353\) 取模。

思路

下文将满足 \(a_{i-1} \lt a_i \gt a_{i+1}\) 的位置 \(i\) 称作波峰,满足 \(a_{i-1} \gt a_i \lt a_{i+1}\) 称作波谷。

由于本题给定的序列是 \(N\) 的全排列,因此不存在相同的整数,相邻两个整数要么前大后小,要么前小后大。

对于相邻三个整数,要么呈现出波峰波谷的变化趋势,要么就是单调上升/下降。如果我们只想统计波峰波谷的数量,那么可以将单调上升/下降的一段子序列中间部分数值全部去除,那么此时序列中除了首尾元素以外,要么就是波峰,要么就是波谷。因此可以得出一个结论,序列中波峰与波谷的数量差值不会超过 \(1\)

而题目中限制的 \(x \gt y\) 则是希望判断波峰严格多于波谷的情况,那么此时符合条件的序列(在去除单调变化的数值后)一定是以波峰开头,并以波峰结尾。那么此时开头的一段一定是单调上升的,结尾的一段一定是单调下降的。

可以发现,一个长度为 \(k\) 的序列 \(a=(a_1,a_2,\dots,a_k)\) 如果是 门松式的,一定符合条件 \(a_1 \lt a_2\)\(a_{k-1} \gt a_k\)。于是题目便变成了找“存在多少个子序列 \(a=(a_1,a_2,\dots,a_k)\) 满足 \(a_1 \lt a_2\)\(a_{k-1} \gt a_k\)”。

接下来开始讨论答案,记 \(k\) 表示答案序列的长度。

明显当 \(k \le 2\) 时是无解的。

\(k = 3\) 时,需要选出三个整数 \(a_1, a_2, a_3\) 满足 \(a_1 \lt a_2 \gt a_3\),此时可以考虑枚举并固定中间值 \(a_2\),假设中间值选的是原排列 \(P\) 中的 \(P_i\) 这一位置,记 \(\text{pre}[i]\) 表示 \(i\) 左侧有多少个小于 \(P_i\) 的整数,记 \(\text{nxt}[i]\) 表示 \(i\) 右侧有多少个小于 \(P_i\) 的整数,那么答案数量便可以加上 \(\text{pre}[i] \times \text{nxt}[i]\)。至于 \(i\) 位置的左右两侧有多少个整数 \(\lt P_i\) 这一问题,可以借助树状数组等数据结构轻松求得。

\(k \ge 4\) 时,尝试固定 \(a_2\)\(a_{k-1}\) 这两个整数,\(a_1\)\(a_k\) 的选择方案仍然可以通过上面 \(k=3\) 时的方法求得。至于 \(a_2\)\(a_{k-1}\) 之间的所有整数,由于并不需要作任何限制,这里记 \(a_2\)\(a_{k-1}\) 两者取的是原排列 \(P\) 中的 \(P_l, P_r\) 两位置,那么 \([l+1, r-1]\) 之间的每个整数便可以任选,中间的选择方案数共 \(2^{r-l-1}\) 种,对答案的总贡献即 \(\text{pre}[l] \times \text{nxt}[r] \times 2^{r-l-1}\)

但很明显这样的 \(O(N^2)\) 枚举并固定两个整数的做法肯定不可行,考虑只固定其中一个整数 \(a_{k-1}\),还是假设 \(a_{k-1}\) 取的是原排列 \(P\) 中的 \(P_r\) 位置的数值。那么此时选择 \(P_r\) 作为子序列倒数第二个元素,对答案的总贡献可以记作:

\[\begin{aligned} & \sum\limits_{l=1}^{r-1} \text{pre}[l] \times \text{nxt}[r] \times 2^{r-l-1} \\ =&\ \text{nxt}[r] \times 2^{r-1} \times \sum\limits_{l=1}^{r-1} \frac{\text{pre}[l]}{2^l} \end{aligned} \]

于是这里的 \(\displaystyle\sum\limits_{l=1}^{r-1} \frac{\text{pre}[l]}{2^l}\) 便可以简单通过前缀和+逆元求得,得出下列代码一。

或者考虑除 \(\text{nxt[r]}\) 这一项以外,其余项随着 \(r\) 从小到大枚举的变化情况。当 \(r\) 变为 \(r+1\) 时,其余项将变为:

\[\begin{aligned} & \sum\limits_{l=1}^{r} \text{pre}[l] \times 2^{r-l} \\ =&\ (\sum\limits_{l=1}^{r-1} \text{pre}[l] \times 2^{r-l}) + \text{pre}[r] \\ =&\ 2 \times (\sum\limits_{l=1}^{r-1} \text{pre}[l] \times 2^{r-l-1}) + \text{pre}[r] \end{aligned} \]

因此可以直接考虑累加递推,每次用一个变量 \(\text{sum}\) 记录 \(\sum\limits_{l=1}^{r-1} \text{pre}[l] \times 2^{r-l-1}\) 的值,当右端点继续变大,只需要每次将其改为 \(2\times \text{sum} + \text{pre}[r]\) 即可。得出下列代码二。

时间复杂度均为 \(O(N\log N)\)

代码一

const long long mod = 998244353;
const long long inv2 = (mod + 1) / 2; // 2 在质数 mod 意义下的逆元

struct BIT
{
    int a[300005], n;
    void init(int _n) // 清空
    {
        n = _n;
        for(int i = 0; i <= n; i++)
            a[i] = 0;
    }
    void update(int p, int x) // p 位置单点加 x
    {
        for(int i = p; i <= n; i += (i & -i))
            a[i] += x;
    }
    int query(int p) // 1 ~ p 前缀求和
    {
        int r = 0;
        for(int i = p; i > 0; i -= (i & -i))
            r += a[i];
        return r;
    }
};

BIT t1, t2;
int n, p[300005];
int pre[300005], nxt[300005];
// pre[i], nxt[i] 分别表示 i 的左侧/右侧有多少个整数小于 p[i]

long long p2[300005], v2[300005]; // 分别表示 2 的幂次与 2 的幂次的逆元

void solve()
{
    int n;
    cin >> n;
    
    p2[0] = v2[0] = 1;
    for(int i = 1; i <= n; i++)
    {
        cin >> p[i];
        p2[i] = p2[i - 1] * 2 % mod;
        v2[i] = v2[i - 1] * inv2 % mod;
    }
    
    t1.init(n);
    t2.init(n);
    
    for(int i = 1; i <= n; i++)
    {
        pre[i] = t1.query(p[i]); // 左侧有多少整数 <= p[i]
        t1.update(p[i], 1);
    }
    for(int i = n; i >= 1; i--)
    {
        nxt[i] = t2.query(p[i]); // 右侧有多少整数 <= p[i]
        t2.update(p[i], 1);
    }
    
    long long ans = 0;
    
    // 当答案长度为 3,枚举中间值 p[i]
    for(int i = 1; i <= n; i++)
    {
        // 左右两侧分别挑一个 < p[i] 的值即可组成答案
        ans = (ans + 1LL * pre[i] * nxt[i]) % mod;
    }
    
    long long sum = 0;
    // 当答案长度 >= 4,枚举答案序列倒数第二个元素为 p[i]
    for(int i = 2; i <= n; i++)
    {
        sum = (sum + pre[i - 1] * v2[i - 1]) % mod;
        ans = (ans + sum * nxt[i] % mod * p2[i - 1]) % mod;
    }
    
    cout << ans;
}

代码二

const long long mod = 998244353;

struct BIT
{
    int a[300005], n;
    void init(int _n) // 清空
    {
        n = _n;
        for(int i = 0; i <= n; i++)
            a[i] = 0;
    }
    void update(int p, int x) // p 位置单点加 x
    {
        for(int i = p; i <= n; i += (i & -i))
            a[i] += x;
    }
    int query(int p) // 1 ~ p 前缀求和
    {
        int r = 0;
        for(int i = p; i > 0; i -= (i & -i))
            r += a[i];
        return r;
    }
};

BIT t1, t2;
int n, p[300005];
int pre[300005], nxt[300005];
// pre[i], nxt[i] 分别表示 i 的左侧/右侧有多少个整数小于 p[i]

void solve()
{
    int n;
    cin >> n;
    
    for(int i = 1; i <= n; i++)
        cin >> p[i];
    
    t1.init(n);
    t2.init(n);
    
    for(int i = 1; i <= n; i++)
    {
        pre[i] = t1.query(p[i]); // 左侧有多少整数 <= p[i]
        t1.update(p[i], 1);
    }
    for(int i = n; i >= 1; i--)
    {
        nxt[i] = t2.query(p[i]); // 右侧有多少整数 <= p[i]
        t2.update(p[i], 1);
    }
    
    long long ans = 0;
    
    // 当答案长度为 3,枚举中间值 p[i]
    for(int i = 1; i <= n; i++)
    {
        // 左右两侧分别挑一个 < p[i] 的值即可组成答案
        ans = (ans + 1LL * pre[i] * nxt[i]) % mod;
    }
    
    long long sum = 0;
    // 当答案长度 >= 4,枚举答案序列倒数第二个元素为 p[i+1]
    for(int i = 1; i < n; i++)
    {
        sum = (sum * 2 + pre[i]) % mod;
        ans = (ans + sum * nxt[i + 1]) % mod;
    }
    
    cout << ans;
}
posted @ 2026-01-03 22:42  StelaYuri  阅读(141)  评论(0)    收藏  举报