2021牛客多校第五场 题解

比赛链接

H题 Holding Two(构造,找规律)

给定正整数 n,m,要求构造一个 nm 列的矩阵 A,元素为0或者1,保证同一行的连续三个元素,同一列的连续三个元素,对角线上的连续三个元素,都不可能相等。

1n,m1000

观察样例,推一推,不难构造出这样一种矩阵:

1100110011001100.... 0011001100110011.... 1100110011001100.... 0011001100110011.... ....

代码如下:

#include<bits/stdc++.h> using namespace std; int n, m; const char output[2][4] = {{'0', '0', '1', '1'}, {'1', '1', '0', '0'}}; int main() { cin >> n >> m; for (int i = 1; i <= n; ++i) { for (int j = 0; j < m; ++j) putchar(output[i % 2][j % 4]); puts(""); } return 0; }

K题 King of Range (ST表/单调队列,双指针)

给定一个长度为 n 的数列 {an},以及 m 次询问。

每次询问给出一个整数 k,问一共存在多少组不同区间,使得区间的最大值减去最小值大于 k

1n105,1m200,1ai,k109

我们可以先 O(nlogn) 建一个 ST 表,这样就可以 O(1) 查询每个区间了(我第一遍用了一个黑心模板,每次查询是 O(logn) 的,害得我超时一次,逆天)。

这题看起来好像没法离线,所以我们必须在线处理,考虑到 m 的范围,我们不难猜测正解里的每次询问复杂度是 O(n) 的,而序列上的 O(n) 算法,很难不想到双指针。

强制在线

显然,倘若区间(l,r)满足要求,那么任何包含该区间的区间同样符合要求,因为扩展区间只会使得最大值更大,最小值更小,两者之差必然更加大于 k 。那么我们不妨双指针进行处理:固定左端点L后,将右端点不断向右移动,当 query(L,R)>k 的时候,给答案加上 nR+1,随后将左端点向右移一位,重复该流程。这个方法是 O(n) 的,最终的总复杂度为 O(nm)

#include <bits/stdc++.h> using namespace std; const int N = 100010; int n, m, a[N]; //ST表 int lg[N]; int ST1[N][25], ST2[N][25]; void init() { // for (int i = 1; i <= n; i++) lg[i] = lg[i - 1] + (1 << lg[i - 1] == i); for (int i = 1; i <= n; i++) lg[i]--; // for (int i = 1; i <= n; ++i) ST1[i][0] = ST2[i][0] = a[i]; for (int k = 1; k <= 20; ++k) for (int i = 1; i + (1 << k) - 1 <= n; ++i) { ST1[i][k] = max(ST1[i][k - 1], ST1[i + (1 << (k - 1))][k - 1]); ST2[i][k] = min(ST2[i][k - 1], ST2[i + (1 << (k - 1))][k - 1]); } } inline int queryMax(int l, int r) { int k = lg[r - l + 1]; return max(ST1[l][k], ST1[r - (1 << k) + 1][k]); } inline int queryMin(int l, int r) { int k = lg[r - l + 1]; return min(ST2[l][k], ST2[r - (1 << k) + 1][k]); } //solve #define LL long long LL solve(int k) { LL ans = 0; for (int l = 1, r = 1; l <= n; ++l) { bool flag = (queryMax(l, r) - queryMin(l, r) > k); while (!flag && r <= n) { r++; if (r == n + 1) break; if (queryMax(l, r) - queryMin(l, r) > k) flag = true; } if (r == n + 1) break; if (flag) ans += n - r + 1; } return ans; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); init(); while (m--) { int k; scanf("%d", &k); printf("%lld\n", solve(k)); } return 0; }

此外,我们也可以用单调队列来维护区间极值,但是并不如 ST 表那么直观好写(主要是我不是很会写),而且这并不能降低本题的复杂度,所以就不贴代码了。

B题 Boxes (数学:概率)

给定 n 个盒子,每个盒子里面有一个白球或者黑球,概率均为 12。打开第 i 个盒子需要 wi 的代价。

我们随时可以花费 C 的代价开挂,知道此时场上还没开的盒子里面有几个黑球。

问采取最优策略的情况下,知道每个盒子装着什么球所花最小代价的数学期望。

1n105,0C,wi109

我们不难想到,采取最优策略,必然是先开代价小的后开代价大的,所以我们将所有盒子按照 wi 从小到大进行排序,复杂度为 O(nlogn)

我们记 P(k) 为前 k 位并非完全相同,但后 nk 位完全相同的情况(注意,这里要发现第 k 位和第 k+1 位要不相同),推出正常状态下 P(k)=(12)nk。特别的,P(0)=(12)n1

得到概率公式之后,我们推出答案公式:

ans=min{C+k=0n1P(k)sum(1,k),sum(1,n)}

sum(1,k) 是开启前 k 个盒子的代价)

做一次前缀和后线性递推即可,复杂度 O(n)

#include<bits/stdc++.h> using namespace std; const int N = 100010; int n; double C, w[N], dp[N]; int main() { //read scanf("%d%lf", &n, &C); for (int i = 1; i <= n; ++i) scanf("%lf", &w[i]); //sort & pre-sum sort(w + 1, w + n + 1); for (int i = 1; i <= n; ++i) w[i] += w[i - 1]; //dp dp[n] = 1.0; for (int i = n - 1; i > 0; i--) dp[i] = dp[i + 1] / 2; dp[0] = dp[1]; //solve double ans = 0; for (int k = 0; k < n; ++k) ans += dp[k] * w[k]; printf("%lf\n", min(ans + C, w[n])); return 0; }

D题 Double Strings (线性DP,组合数)

给定两个字符串 A,B,要求我们从中各选出一个子串,分别记为 s,t(sA,tB),且满足:

  1. |s|=|t|=nn 是一个正整数
  2. i[1,n],si<ti
  3. j[1,i),sj=tj

求出不同组合 (s,t) 的总个数(这里的不同,是指字符对应的下标不同)。

1|A|,|B|5000,两个字符串均只包含小写字母

简单来说,这两个字符串存在某一位具有大小关系,前面部分完全相同,后面部分随便即可。

求公共前缀数量可以预处理,是一个典型的二维 DP,记 fi,jA 的前 i 位, B 的前 j 位所能构成的公共子序列的数量,有如下 DP 方程,边界条件为 f0,=f,0=0,复杂度 O(n2)

fi,j=(fi1,j+fi,j1fi1,j1)+{fi1,j1+1,Ai=Bj0,AiBj

值得一提的是,本题允许这个前缀是空的,所以统计答案的时候,还要对每个 fi,j 加上一才对。

后面的就是排列组合算了,假设后面 A 还剩下 x 位,B 还剩下 y 位,那么后面的可能的后缀长度为

g=i=0min(x,y)CxiCyi

我们假设 xy,根据组合的对称性,可得

g=i=0xCxxiCyi=CxxCy0+Cxx1Cy1++Cx0Cyx

这东西如果在高中做,我感觉我想的可能会快一点:二项式定理!

(1+a)x=Cx0a0+Cx1a1++Cxxax,(1+a)y=Cy0a0+Cy1a1++Cyyay,那么便有

(1+a)x(1+a)y=i=0x+yj=0iCxjCyijai

(附:当 m>n 时,记 Cnm=0

也就是说,ax 的系数恰好就是 g。又因为 (1+a)x(1+a)y=(1+a)x+y,所以我们得到

g=i=0xCxxiCyi=CxxCy0+Cxx1Cy1++Cx0Cyx=Cx+yx

我们也先 O(n2) 的预处理出所有的组合数即可。

考虑到这个数据规模,不太好用杨辉三角来预处理组合数,所以我们必须用别的方法来 O(1) 的处理它。因为需要对分数取模,所以显然是要预处理所有阶乘的乘法逆元(abmodp=ainv(b)modp)。

根据费马小定理,可知 inv(a)=ap2modp,所以我们可以先算出 n! 的逆元。

随后,我们便可以线性递推出其他阶乘的逆元:

n!inv(n!)1modp(n1)!inv((n1)!)1modpn!inv((n1)!)nmodpn!inv(n!)inv((n1)!)ninv(n!)modpinv((n1)!)ninv(n!)modp

然后我们便可以 O(1) 的计算组合数了:

Cnm=n!inv(m!)inv((nm)!)modp

处理好了所有的前缀和后缀,我们便可以 O(n2) 的枚举中间断点,随后 O(1) 的查询所有的前缀和后缀,并计入答案即可。本题虽然要算的东西贼多,但是最终的总复杂度还是 O(n2) 的。

#include <bits/stdc++.h> #define LL long long using namespace std; const int N = 5010; const LL mod = 1e9 + 7; //逆元法快速求组合数 LL fact[N << 1], inv[N << 1]; LL quickpow(LL a, LL b) { LL res = 1; while (b) { if (b & 1) res = res * a % mod; b >>= 1; a = a * a % mod; } return res; } void init(int n) { fact[0] = 1; for (int i = 1; i <= n * 2; ++i) fact[i] = fact[i - 1] * i % mod; inv[n] = quickpow(fact[n], mod - 2); for (int i = 2 * n - 1; i >= 0; --i) inv[i] = inv[i + 1] * (i + 1) % mod; } LL C(LL n, LL m) { if (m > n) return 0; return fact[n] * inv[m] % mod * inv[n - m] % mod; } // int n, m; char s[N], t[N]; LL f[N][N]; // int main() { scanf("%s%s", s + 1, t + 1); n = strlen(s + 1), m = strlen(t + 1); init(max(n, m) << 1); //DP for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) { f[i][j] = (f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + mod) % mod; if (s[i] == t[j]) f[i][j] = (f[i][j] + f[i - 1][j - 1] + 1) % mod; } //clac ans LL ans = 0; for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) if (s[i] < t[j]) ans = (ans + (1 + f[i-1][j-1]) * C(n - i + m - j, m - j)) % mod; printf("%lld", ans); return 0; }

J题 Jewels (二分图带权匹配)

现在有 n 个气球,第 i 个气球的坐标为 (xi,yi,zi)。每个气球有一个速度 vi,也就是说,时刻 t 的时候,第 i 个气球的坐标为 (xi,yi,zi+tvi)

我们在点 (0,0,0) 处。每个时刻(从0开始)都可以拿走一个气球,同时花费 d2 的体力(若拿走的这个气球在 (a,b,c) 处,那么 d2=a2+b2+c2)。问我们应该采取怎样的顺序,才能花费最少的体力,拿走所有的气球,并输出花费体力的最小值。

1n300,0|xi|,|yi|,zi,vi1000,所有数据均为整数

显然,每个点的 xi,yi 坐标对于答案并无影响,我们只需要正常将其加上去就好了。那么,我们便可以将这个问题从三维空间系化到了一维的数轴上。

排完序之后,我们需要使 i=0n1(zi+ivi)2 最大,也就是使 i=0n1vi2i2+2zivii 最大,......


以上没啥子用,下面直接看正解:

显然,用n次便可以拿走所有的气球,也就是说,我们需要将这几个时间和对应拿走的气球一一对应,我考试的时候想到的是基于临项交换的贪心什么什么的,但是这题让我属实开眼界了:构建一个二分图,左边是各个时间点,右边是气球,两边之间的边权则是对应时间所花的体力(一种基于时间的建模方式)。显然,这是一个二分图的带权匹配问题。(但有一说一,我记得不知道在哪看过,当数据规模在100-300 这一级的时候,要么是 Floyd 或者各种 DP,要么就是网络流/二分图了)

因为这显然是一个完备匹配,而且图十分稠密,所以可以使用 KM 算法来完成。(这题数据贼逆天,费用流和基于 DFS 的 KM 算法全部都 TLE 了,必须得用 BFS 优化过的 KM 算法,或者用其他奇奇怪怪方法对普通KM进行优化,才能完成这题)

#include <bits/stdc++.h> using namespace std; #define LL long long const int N = 310; #define INF ((1LL) << 62) int n; LL dis[N][N]; //KM int vis[N], match[N], pre[N]; LL la[N], ra[N], slack[N]; void bfs(LL u) { LL x, y = 0, yy = 0, delta; memset(pre, 0, sizeof(pre)); for (int i = 1; i <= n; i++) slack[i] = INF; match[y] = u; while (true) { x = match[y], delta = INF, vis[y] = 1; for (int i = 1; i <= n; i++) { if (vis[i]) continue; if (slack[i] > la[x] + ra[i] - dis[x][i]) { slack[i] = la[x] + ra[i] - dis[x][i]; pre[i] = y; } if (slack[i] < delta) delta = slack[i], yy = i; } for (int i = 0; i <= n; i++) if (vis[i]) { la[match[i]] -= delta; ra[i] += delta; } else slack[i] -= delta; y = yy; if (match[y] == -1) break; } while (y) { match[y] = match[pre[y]]; y = pre[y]; } } LL KM() { memset(match, -1, sizeof(match)); memset(la, 0, sizeof(la)); memset(ra, 0, sizeof(ra)); for (int i = 1; i <= n; i++) { memset(vis, 0, sizeof(vis)); bfs(i); } LL ans = 0; for (int i = 1; i <= n; i++) ans += dis[match[i]][i]; return -ans; } int main() { //read & build scanf("%d", &n); for (int i = 1; i <= n; ++i) { LL x, y, z, v; scanf("%lld%lld%lld%lld", &x, &y, &z, &v); for (int t = 0; t < n; ++t) dis[t + 1][i] = -x * x - y * y - (z + t * v) * (z + t * v); } //solve printf("%lld", KM()); return 0; }

__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/15108767.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(108)  评论(1编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示