2022哈理工校赛 题解
L题 NP-hard
给定十进制下的正整数 \(n\),问其在 \(x,y\) 进制下哪个 1 比较多?
\(1\leq n \leq 10^9,2\leq x, y\leq 10\)
#include<bits/stdc++.h>
using namespace std;
int f(int n, int x) {
int res = 0;
while (n) {
res += n % x == 1;
n /= x;
}
return res;
}
char solve()
{
int n, x, y;
cin >> n >> x >> y;
int a = f(n, x), b = f(n, y);
if (a == b) return '=';
else if (a > b) return '>';
else return '<';
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
I题 又AK了
计算出每题所需要的时间,然后先做花时间小的在做花时间大的,数学证明如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 100;
int n, a[N], b[N];
int solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i)
b[i] = a[i] - a[i - 1];
sort(b + 1, b + n + 1);
for (int i = 1; i <= n; ++i)
b[i] += b[i - 1];
int res = 0;
for (int i = 1; i <= n; ++i) res += b[i];
return res;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
G题 gk的数字游戏
我们有一个游戏:
- 首先给定两个正整数 \(n,m\)
- 执行操作\(\begin{cases}n=n-m&n\geq m\\m=m-n&n<m\end{cases}\)
- 反复执行操作2,直到某个数变为0
问总操作步数。
\(0\leq n,m\leq 10^9\)
这题不难想到辗转相除法及其优化:我们用取模代替减法,然后通过除法来记录操作次数,反复模拟即可。
#include<bits/stdc++.h>
using namespace std;
int solve()
{
int n, m;
cin >> n >> m;
int res = 0;
while (n > 0 && m > 0) {
if (n <= m) swap(n, m);
res += n / m;
n %= m;
}
return res;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
E题 gk的字符串
给定一个仅包含小写字母和问号的字符串,我们现在要把每个问号都替换成小写字母,且要求不存在两个相邻且相同的字符。
\(|s|\leq 10^6\)
贪心的替换即可,a b c 轮着试。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
char s[N];
void solve()
{
scanf("%s", s + 1);
int n = strlen(s + 1);
s[0] = s[n + 1] = '\0';
for (int i = 1; i <= n; ++i) {
if (s[i] != '?') continue;
for (char c = 'a'; c <= 'z'; ++c)
if (s[i - 1] != c && s[i + 1] != c) {
s[i] = c;
break;
}
}
}
int main()
{
int T;
cin >> T;
while (T--) {
solve();
puts(s + 1);
}
return 0;
}
J题 大数乘法
给定 \(x,y,p\),求出 \(x^y\bmod p\) 的值。
\(0\leq x \leq 10^5,0\leq y\leq 10^{100000},10^5\leq p\leq 10^9+7\)。
方法一:欧拉降幂
#include <bits/stdc++.h>
using namespace std;
#define LL long long
LL phi(LL n) {
LL ans = n;
for (LL i = 2; i * i <= n; ++i)
if (n % i == 0) {
ans = ans / i * (i - 1);
while (n % i == 0) n /= i;
}
if (n > 1) ans = ans / n * (n - 1);
return ans;
}
const int N = 1000010;
char str[N];
LL read(LL mod, bool &flag) {
int n = strlen(str + 1);
LL ans = 0;
for (int i = 1; i <= n; ++i) {
ans = ans * 10 + str[i] - '0';
if (ans >= mod) flag = true;
ans %= mod;
}
return ans;
}
LL power(LL a, LL b, LL mod) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
LL solve() {
LL a, b, m;
scanf("%lld%s%lld", &a, str + 1, &m);
LL P = phi(m);
bool flag = false;
b = read(P, flag);
if (flag) b += P;
return power(a, b, m);
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
方法二:预处理(来自官方题解)
令 \(y=\sum\limits_{i=0}^n a_i*10^i\),然后便有
那么我们预处理好 \(x^{10^i}\) 即可。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
LL x, p, px[N];
char y[N];
LL calc(LL a, LL b) {
LL res = 1;
while (b--) res = res * a % p;
return res;
}
LL solve() {
scanf("%lld%s%lld", &x, y, &p);
int n = strlen(y) - 1;
px[0] = x;
for (int i = 1; i <= n; ++i)
px[i] = calc(px[i - 1], 10);
LL res = 1;
for (int i = 0; i <= n; ++i)
res = res * calc(px[i], y[n - i] - '0') % p;
return res;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
(涨见识了,高位次的幂原来还可以这么求)
B题 抓球
从 \(n\) 个黑球,\(m\) 个白球中任选 \(k\) 个球,问连续 \(q\) 次都是 \(k\) 个黑球的概率。
\(1\leq n,m\leq 10^5,1\leq k\leq n+m,1\leq q \leq 10^{12}\),结果对 \(10^9+7\) 取模。
显然,答案为
那么掌握线性求组合数和分数取模即可。
#include<bits/stdc++.h>
using namespace std;
//逆元法快速求组合数
#define LL long long
const LL mod = 1e9 + 7;
const int N = 1000010;
LL fact[N << 1], inv[N << 1];
//快速幂
LL power(LL a, LL b) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
LL getinv(LL x) {
return power(x, mod - 2) % mod;
}
void init(int n) {
fact[0] = 1;
for (int i = 1; i <= n; ++i)
fact[i] = fact[i - 1] * i % mod;
inv[n] = power(fact[n], mod - 2);
for (int i = 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 main()
{
init(500000);
int T;
cin >> T;
while (T--) {
LL n, m, k, q;
cin >> n >> m >> k >> q;
cout << power(C(n, k) * getinv(C(n + m, k)) % mod, q) << endl;
}
return 0;
}
F题 gk的树
给定一个 \(n\) 点的树,问至少需要删多少边,才可以使得每个点的度数都不超过 \(k\)?
\(1\leq n,k\leq 10^5\)
方法一:树形DP
对于一个点来说,如果我们要删边,那么无非:
- 删除其向子节点所连的边
- 删除其向父节点所连的边(根节点除外)
那么,我们记 \(dp_{x,0/1}\) 分别为删除/不删除 \(x\) 节点向父亲的所连边,且使得整个子树都符合度数小于等于 \(k\) 所需删除的最少边数。不太显然(我也不会证明),\(dp_{x,0}\leq dp_{x,1}\)。
我们考虑怎么进行计算:
- 先初始化:\(dp_{x,0}=0,dp_{x,1}=1\)
- 首先递归计算所有子树
- 我们假设该节点不需要删点,那么把所有子树的 \(dp_0\) 值加上去
- 考虑删边,我们想一下怎么删最好:对于统计答案而言,删边等同于用对应连向子树的 \(dp_1\) 来替换 \(dp_0\) 值;那么,我们直接对于所有子树,按照 \(dp_1-dp_0\) 来排序,然后贪心选择最小的
最后答案为 \(dp_{root,0}\),总复杂度 \(O(n\log n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = 200010;
int n, k;
vector<int> G[N];
int dp[N][2];
void dfs(int x, int fa) {
dp[x][0] = 0, dp[x][1] = 1;
vector<int> vec;
for (int y : G[x])
if (y != fa) {
dfs(y, x);
vec.push_back(y);
}
sort(vec.begin(), vec.end(), [](int a, int b) {
return dp[a][1] - dp[a][0] < dp[b][1] - dp[b][0];
});
//dpx0
for (int y : vec) dp[x][0] += dp[y][0], dp[x][1] += dp[y][0];
int cnt = G[x].size() - k;
for (int i = 0; i < cnt; ++i) {
int y = vec[i];
dp[x][0] += dp[y][1] - dp[y][0];
}
//dpx1
for (int i = 0; i < cnt - 1; ++i) {
int y = vec[i];
dp[x][1] += dp[y][1] - dp[y][0];
}
}
int solve() {
cin >> n >> k;
for (int i = 1; i <= n; ++i) G[i].clear();
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, 0);
return dp[1][0];
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
方法二:贪心
事实上,我们发现 \(dp_{x,0}\leq dp_{x,1}\leq dp_{x,0}+1\),这意味着我们其实可以将上面的树形 DP 中排序的那一部分复杂度降下来,使得总复杂度降到 \(O(n)\)。(本质上其实就是一种小贪心了属于是)
方法三:网络流
这方法真的是让我小刀喇屁股——开了大眼了。
因为树必然是一个二分图,所以我们对树进行黑白染色,然后从源点 S 向黑点连容量为 k 的边,黑点向白点连容量为 1 的边,白点向汇点 T 连容量为 k 的边,然后用 Dinic 跑一次最大流,所得结果即为可以保留的边的最大数量。(通过建模,我们巧妙将度数的限制转化为了流量的限制:每个点都至多流入/流出 k 的流量。
Dinic 跑网络流的上限复杂度为 \(O(n^2m)\),但是一般来说都跑不到上限,可以解决 \(10^4\) 到 \(10^5\) 大小的图。特别的,对于这类二分图跑网络流,复杂度是 \(O(m\sqrt{n})\) 的。
C题 迷宫
给定一整个 \(n*m*h\) 的三维方格空间,外加一个空的点集 \(S\)(初始状态下 \(S\) 为空)。现在,我们有 \(q\) 次操作,分两类:
- 向点集中塞入一个点 \(x\)
- 给定一个点 \(x\),问这个点到点集内某个点的最短路径(即 \(\min\limits_{y\in S}\operatorname{dis}(x,y)\))(两点之间的曼哈顿距离)
\(1\leq q,nmh\leq 10^5\)
我们考虑下面两个问题(新学的):
-
给定若干个点构成的点集,怎么计算空间中所有点到这个点集的最短路径?
直接将这些点全部推入一个队列,并将 \(dis_x\) 设置为 0,然后开始 BFS 一遍即可。(可以想象成从一个虚点向他们连边,然后从这个点开始 BFS 即可)
-
对于上面的问题,如果我们需要动态加入新点,怎么办?
实际上,我们在原来 dis 数组上操作,将新点的 dis 值设为 0,然后再次 BFS 即可。(最短路中的动态加点问题)
处理好了这个问题,我们发现本题拥有类似其他题(我说不上来,但是有印象)的性质:
- 当点集数量小于某个数的时候,不如直接将待查询点和点集里面的点一一计算并比较
- 如果点集数量有点大,那么我们不如直接 BFS 一遍,然后每次查询都可以 \(O(1)\) 查询(如果后面不需要插入点的话)
对于上面那个点集,如果需要插入点的话,我们不妨先用一个 vec 备着,在数量比较小的时候先暴力,等到一定数量了再插进去 BFS。
好了,那么这个数,我们应该设置成多少呢?
熟悉分块的话,不难想到,记 \(N=10^5\),那么这个数就记为 \(\sqrt{N}\):
- 插入新点进入集合,等到集合达到 \(\sqrt{N}\) 时就将其全部推入队列来重构,均摊复杂度 \(O(\sqrt{N})\)
- 对于查询,在之前的点直接 dis \(O(1)\) 查询,集合里面直接 \(O(\sqrt{N})\) 来查询,均摊同样复杂度
总复杂度 \(O(N\sqrt{N})\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
const int LIM = 1000;
int n, m, h, Q;
struct P { int x, y, z; };
vector<P> vec;
inline int getid(P A) {
return h * (A.x * m + A.y) + A.z;
}
const int dx[6] = {-1, 1, 0, 0, 0, 0}, dy[6] = {0, 0, 1, -1, 0, 0}, dz[6] = {0, 0, 0, 0, 1, -1};
bool in(int x, int y, int z) {
return x >= 1 && x <= n && y >= 1 && y <= m && z >= 1 && z <= h;
}
int dis[N];
queue<P> q;
void BFS() {
for (P x : vec) {
q.push(x);
dis[getid(x)] = 0;
}
vec.clear();
while (!q.empty()) {
P point = q.front(); q.pop();
int x = point.x, y = point.y, z = point.z;
int pid = getid(point);
for (int i = 0; i < 6; ++i) {
int nx = x + dx[i], ny = y + dy[i], nz = z + dz[i];
int nid = getid({nx, ny, nz});
if (in(nx, ny, nz) && dis[nid] > dis[pid] + 1) {
q.push({nx, ny, nz});
dis[nid] = dis[pid] + 1;
}
}
}
return;
}
int getdis(P A, P B) {
return abs(A.x - B.x) + abs(A.y - B.y) + abs(A.z - B.z);
}
void solve() {
cin >> n >> m >> h >> Q;
memset(dis, 0x3f, sizeof(dis));
vec.clear();
while (Q--) {
int opt, x, y, z;
scanf("%d%d%d%d", &opt, &x, &y, &z);
if (opt == 1) {
vec.push_back({x, y, z});
if ((int)vec.size() >= LIM) BFS();
}
else {
int id = getid({x, y, z}), ans = dis[id];
for (P point : vec)
ans = min(ans, getdis(point, {x, y, z}));
cout << ans << endl;
}
}
}
int main()
{
int T;
cin >> T;
while (T--) solve();
return 0;
}