2022 牛客多校第四场 题解

比赛链接

A题 Task Computing(临项交换法贪心,DP)

我们先考虑在选定了是哪些服务器的情况下咋排列:

\[\sum\limits_{i=1}^mw_{a_{i}}\prod_{j=0}^{i-1}p_{a_j} \]

排序一下,变为 \(\sum\limits_{i=1}^mw_i\prod\limits_{j=0}^{i-1}p_j\),那么考虑临项交换法,选定相邻项 \((w_x,p_x),(w_y,p_y)(y=x+1)\) 进行交换:

  1. \((w_x,p_x,w_y,p_y)\),对应的相关值为 \(w_x\prod\limits_{j=0}^{x-1}p_j+w_y(\prod\limits_{j=0}^{x-1}p_j)*p_x= \prod\limits_{j=0}^{x-1}p_j(w_x+w_yp_x)\)
  2. \((w_y,p_y,w_x,p_x)\),对应的相关值为 \(w_y\prod\limits_{j=0}^{x-1}p_j+w_x(\prod\limits_{j=0}^{x-1}p_j)*p_y= \prod\limits_{j=0}^{x-1}p_j(w_y+w_xp_y)\)

要求 \(w_x+w_yp_x>w_y+w_xp_y\),可得 \(\frac{1-p_y}{w_y}>\frac{1-p_x}{w_x}\),所以我们排序的时候应该优先把 \(\frac{1-p}{w}\) 小的排在前面。

把服务器排好之后,那就是考虑怎么选的问题了:我们考虑动态规划,令 \(f_{i,j}\) 为从前 \(i\) 个服务器中选取 \(j\) 台所得到的最大值,\(g_{i,j}\) 表示相对应的累乘值,可是这种方式存在一个小漏洞:\(f_{i,j}\) 最大时 \(g_{i,j}\) 并不一定最优,不满足最优子结构性质。

很难想象出题人是在怎样的心理状态下搞出了这题的标程:用 \(f_{i,j}\) 表示从 \([i,n]\) 中选择了 \(j\) 件服务器所能达到的最大值(也就是反向枚举),此时 DP 方程就变成了:

\[dp_{i,j}=\max(dp_{i+1,j},dp_{i+1,j-1}*p_i+w_i) \]

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
struct Node { double w, q; } a[N];
//第一维可以压,但没必要
double dp[N][21];
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> a[i].w;
    for (int i = 1; i <= n; ++i) cin >> a[i].q;
    sort(a + 1, a + 1 + n, [](const Node &A, const Node &B) {
        return (1e4 - A.q) / A.w < (1e4 - B.q) / B.w;
    });
    for (int i = n; i >= 1; --i)
        for (int j = m; j >= 1; --j)
            dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - 1] * (a[i].q / 1e4) + a[i].w);
    printf("%.10f\n", dp[1][m]);
    return 0;
}

D题 Jobs (Easy Version)(状压,前缀和)

给定 \(n\) 家公司,第 \(i\) 家公司有 \(m_i\) 个岗位,每个岗位有 \((x,y,z)\) 的对 IQ, EQ, AQ 的要求,只有三项均达标的人才能收到 offer。

现在有 \(q\) 个人前来询问,想要知道他能够进入多少家公司(只要能进一个岗位就行)?

\(n\leq 10,m\leq 10^5,q\leq 2*10^6,1\leq x,y,z\leq 400\),强制在线

我们考虑简化版本:仅有一家公司,并且要求仅有一种:我们直接开一个值域大小的数组 \(s\),对于岗位要求为 \(x\) 的工作,直接令 \(s_x=1\) 即可。对于每次询问 \(t\),我们仅需要查询 \([1,t]\) 上是否存在 1 即可,而这可以前缀和优化。

额,大家显然能够想到普通的前缀和维护,然后查询区间上 1 的个数,这种想法很普遍,但是不利于思考下面的状压优化。来考虑一下,如果要求每一个位置只能用 1bit 维护,大家会怎么写呢?

处理好了这个,我们来将其扩展一下:

  1. 对于三个维度的情况

    显然,问题从一维前缀和问题变为了三维前缀和问题,改一下就行了。考虑到 \(x,y,z\leq 400\),复杂度并非不能接受。

    如果不是强制在线,那么本题就已经解决了:一个公司放进前缀和数组,然后对于每个查询来算贡献,复杂度 \(O(n(m+q+400^3))\),有点卡常,但是对于牛客来说应该还是可以接受的。遗憾的是,由于强制在线,所以我们必须一次查出所有公司的状况,而开 \(n\) 个这样的前缀和数组也不适用。

  2. 对于多个公司的情况

    上面我们提到仅用一个 bit 维护,是因为我们可以这样写:s[i]|=s[i-1],虽然不满足了前缀和的可减性质,但是我们反正查询的也是前缀,所以倒也没有影响。

    一个 bit 的占据空间是一个 int 的 \(\frac{1}{32}\),所以如果仅用 bit 的话,空间是足够维护的,不过 C++ 并没有这么基础的数据类型,我们也不想上 bitset,那么......

    实际上,我们直接在一个 int 里面进行状态压缩即可,对于第 \(k\) 家公司,如果存在一个要求为 \((x,y,z)\) 的工作,那么就令 \(s_{x,y,x}|=(1<<k)\)。做一次前缀和后,\((x,y,z)\) 就表示了该状态下能够合格的公司的状态,统计该 int 内 1 的个数即可。(其实就是把 int 当作 bit[32] 就行了)。

我们将两者相结合,即可求出答案,空间复杂度 \(O(400^3)\),时间复杂度 \(O(n\sum m_i +q+400^3)\)

#include<bits/stdc++.h>
using namespace std;
const int V = 410;
int n, q, seed, s[V][V][V];
int solve(int x, int y, int z) {
    int res = 0;
    for (int v = s[x][y][z]; v; v >>= 1)
        if (v & 1) ++res;
    return res;
}
int main()
{
    //read
    scanf("%d%d", &n, &q);
    for (int i = 0, m, x, y, z; i < n; ++i) {
        scanf("%d", &m);
        for (int j = 0; j < m; ++j) {
            scanf("%d%d%d", &x, &y, &z);
            s[x][y][z] |= (1 << i);
        }
    }
    //init
    for (int i = 1; i <= 400; ++i)
        for (int j = 1; j <= 400; ++j)
            for (int k = 1; k <= 400; ++k)
                s[i][j][k] |= s[i - 1][j][k];
    for (int i = 1; i <= 400; ++i)
        for (int j = 1; j <= 400; ++j)
            for (int k = 1; k <= 400; ++k)
                s[i][j][k] |= s[i][j - 1][k];
    for (int i = 1; i <= 400; ++i)
        for (int j = 1; j <= 400; ++j)
            for (int k = 1; k <= 400; ++k)
                s[i][j][k] |= s[i][j][k - 1];
    //solve
    scanf("%d", &seed);
    mt19937 rng(seed);
    uniform_int_distribution<> u(1, 400);
    int lastans = 0;
    long long res = 0, mod = 998244353;
    for (int i = 1; i <= q; ++i) {
        int x = (u(rng) ^ lastans) % 400 + 1;
        int y = (u(rng) ^ lastans) % 400 + 1;
        int z = (u(rng) ^ lastans) % 400 + 1;
        lastans = solve(x, y, z);
        res = (res * seed + lastans) % mod;
    }
    printf("%lld", res);
    return 0;
}

H题 Wall Builder II(构造)

给定数字 \(n\),相当于给定了 1x1 大小的砖块 \(n\) 块,1x2 大小的砖块 \(n-1\) 块,一直到 1xN 大小的砖块 \(1\) 块。

现在,尝试要求用这些砖块拼出一个矩形出来(方块必须按照 1xN 的形式放置,不可以竖起来),要求拼出来的矩形尽可能小。

\(n\leq 100,\sum n\leq 200\)

给定 \(n\) 时,可得砖块面积之和为 \(\sum\limits_{i=1}^ni(n+1-i)=C_{n+2}^3\),我们尝试质因数分解成 \(ab\),然后看看能不能组成 \(a\)\(b\) 列(\(a<b\),因为这种情况下更容易拼出来)的矩阵。对于矩阵,我们可以直接贪心的用,每次都尽力选长度长的。

显然,当 \(b-a\) 最小的时候,周长最短,且打表发现,对于 \(n\leq 100\),确实都存在合法解。

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, t[N];
int getfac(int x) {
    int res = 1;
    for (int i = 2; i * i <= x; i++)
        if (x % i == 0) res = i;
    return res;
}
int main()
{
    int T;
    cin >> T;
    while (T--) {
        cin >> n;
        for (int i = 1; i <= n; i++)
            t[i] = n + 1 - i;
        int sum = n * (n + 1) * (n + 2) / 6;
        int a = getfac(sum), b = sum / a;
        printf("%d\n", 2 * (a + b));
        for (int i = 1; i <= a; i++) {
            int flag = 0;
            for (int j = n; j >= 1; j--) {
                while (t[j] > 0 && flag + j <= b) {
                    flag += j, t[j]--;
                    printf("%d %d %d %d\n", flag - j, i - 1, flag, i);
                }
                if (flag == b) break;
            }
        }
    }
    return 0;
}

K题 NIO's Sword(数学)

现在有 \(n\) 个怪物,分别在位置 1 到 \(n\)。我们必须逐步前行,一个一个击败怪物。

当我们位于位置 \(i-1\) 时,想要击败位置 \(i\) 的怪物时,必须要求攻击力 \(A\equiv i(\bmod n)\)(初始状态下,\(A=0\)

我们并不可能一路那么顺的打下去,所以我们在任意时刻强化自己的攻击力,使得自己的攻击力从 \(A\) 变成 \(10A+x\)\(x\) 是自选的,位于 \([0,9]\) 间的一个整数,换句话说,强化可以使得攻击力从 \(A\) 变为 \([10A,10A+9]\) 间的一个整数)。

问,我们至少需要多少次强化,才能击败所有的怪物?

\(1\leq n\leq 10^6\)

\(n=1\) 时候不用强化,原因显然。

\(n>1\) 的时候,我一开始还在想,有没有啥当前状态对后面影响啥的,后来发现自己想多了:当我们位置处于 \(i\) 时,攻击力 \(A\)\(n\) 取模的值就是 \(i\)。因为取模意义下等价,所以就相当于攻击力为 \(i\)。为了击败后面的怪物 \(i+1\),我们必须花费最少次数,使得自己的攻击力 \(A\)\(i\) 变为同 \(i+1\) 同余的一个数。

对于攻击力 \(A\),一次强化后可以变为 \(10A+x\in[10A,10A+9]\),两次变化后就成了 \(100A+10x+y\in[100A,100A+99]\),以此类推。因而,我们得出结论:经过 \(k\) 次强化,\(A\) 可以变成 \([10^kA,10^kA-(10^k-1)]\) 里面的任意一个数,要判断区间内有没有和 \(i+1\) 同于,那么就相当于判断 \([10^kA-(i+1),10^kA-(10^k-1)-(i+1)]\) 里面有没有 \(n\) 的倍数。而判断区间内 \(x\) 的倍数的个数,直接采用类似前缀和的思维方式即可做到:

//0<l<=r
int calc(int l, int r, int x) {
    return (r / n) - ((l - 1) / n);
}

\(k\) 次强化后对应的区间长度大致为 \(10^k\),因此只要至多 6-7 次即可得到答案,因此我们直接暴力枚举即可,复杂度 \(O(n\log_{10}^n)\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
int n;
//注意,必须0<l<=r,我这里被卡了一下
bool check(LL l, LL r) { return r / n > (l - 1) / n; }
int main()
{
    cin >> n;
    if (n == 1) { printf("0"); return 0; }
    int ans = 1;
    for (int i = 1; i < n; ++i) {
        LL L = i, R = i;
        for (int k = 1; k <= 10; ++k) {
            L = 10 * L, R = 10 * R + 9;
            if (check(L - (i + 1), R - (i + 1))) { ans += k; break; }
        }
    }
    cout << ans;
    return 0;
}

N题 Particle Arts(数学)

给定 \(n\) 个非负整数 \(\{a_n\}\)。接下来,我们会随机的选取两个数 \(a_i,a_j\),删除他们,并插入两个新数字 \(a_i|a_j,a_i\&a_j\)

经过无穷次操作后,整个数列的方差会趋于稳定,试求出该值。

\(2\leq n\leq 10^5,0\leq a_i<2^{15}\)

方差趋于稳定,我个人认为意味着整个数列不再变化,即任意两数被选取后,操作本身不产生变化,例如选取 \(a,b\),有 \(a|b=a,a\&b=b\)。换一个说法,任意两个数,在二进制上都有集合的从属关系。

那么,我们直接统计每一位上 1 的数量,然后统一下放,得到新数列,随后计算这个数列的方差即可。

本题很卡数据范围,要用 __int128 才行。

#include<bits/stdc++.h>
using namespace std;
#define LL __int128
LL gcd(LL a, LL b) {
    if (!b) return a;
    return gcd(b, a % b);
}
void write(LL x) {
    if (x > 9) write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 100010;
int n, v[15];
LL a[N];
int main()
{
    scanf("%d", &n);
    for (int i = 1, x; i <= n; ++i) {
        scanf("%d", &x);
        for (int k = 0; k < 15; ++k)
            if ((x >> k) & 1) v[k]++;
    }
    for (int i = 1; i <= n; ++i) {
        a[i] = 0;
        for (int k = 0; k < 15; ++k)
            if (v[k]) a[i] |= (1 << k), v[k]--;
    }
    if (a[1] == a[n]) { printf("0/1"); return 0; }
    //
    LL s = 0;
    for (int i = 1; i <= n; ++i) s += a[i];
    LL A = 0, B = n;
    B = B * B * B;
    for (int i = 1; i <= n; ++i) {
        LL d = n * a[i] - s;
        A += d * d;
    }
    LL d = gcd(A, B);
    LL AA = A / d, BB = B / d;
    write(AA); putchar('/'); write(BB);
    return 0;
}
posted @ 2022-07-31 11:33  cyhforlight  阅读(31)  评论(0编辑  收藏  举报