2022 牛客多校第四场 题解
A题 Task Computing(临项交换法贪心,DP)
我们先考虑在选定了是哪些服务器的情况下咋排列:
排序一下,变为 \(\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)\) 进行交换:
- \((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)\)
- \((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 方程就变成了:
#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 维护,大家会怎么写呢?
处理好了这个,我们来将其扩展一下:
-
对于三个维度的情况
显然,问题从一维前缀和问题变为了三维前缀和问题,改一下就行了。考虑到 \(x,y,z\leq 400\),复杂度并非不能接受。
如果不是强制在线,那么本题就已经解决了:一个公司放进前缀和数组,然后对于每个查询来算贡献,复杂度 \(O(n(m+q+400^3))\),有点卡常,但是对于牛客来说应该还是可以接受的。遗憾的是,由于强制在线,所以我们必须一次查出所有公司的状况,而开 \(n\) 个这样的前缀和数组也不适用。
-
对于多个公司的情况
上面我们提到仅用一个 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;
}