2023 百度之星 题目解析
简介
2023 百度之星已经结束很久了,第3、5题是较为简单的打卡题,其余难度均为省选难度,不得不说,现在水题越来越少了
| 编号 | 题目名称 | 题目链接 | 通过状态 |
|---|---|---|---|
| 1 | 石碑文 | 石碑文 | AC |
| 2 | 染色游戏 | 染色游戏 | AC |
| 3 | 新的阶乘 | 新的阶乘 | AC |
| 4 | 炼金术 | 炼金术 | AC |
| 5 | 新材料 | 新材料 | AC |
| 6 | 与蝴蝶一起消散吧 | 与蝴蝶一起消散吧 | AC |
| 7 | 魔法阵 | 魔法阵 | 34/35 |
| 8 | 竞猜 | 竞猜 | AC |
题目解析
一.石碑文
题目描述
现在小度在石碑上找到了一些文字,这些文字包含\(N\)个英文字符,这些文字依稀可以辨认出来,另一些文字难以辨认,在可以辨认出来的文字中,小度发现了他喜欢的文字\(shs\),小度习惯把喜欢的事物说三遍及以上,他希望知道原始的石碑上有多少种可能性会出现三次及以上\(shs\)(三个\(shs\)不能出现重合,即\(shshs\)只能算出现一次\(shs\)),这样的碑文可能有很多,你只需要输出答案对1e9+7取模的结果即可。
输入格式
一行输入的是整数 \(n\) \((1 \leq n \leq 10^6)\),表示碑文长度 。
输出格式
一行一个数字,表示有多少种字符串可能出现了三个及以上\(shs\)。
样例输入
10
样例输出
104
题目分析
首先我们发现此题需要3个\(shs\)串才能达到计数标准,那么我们就可以列出要达到的状态的几种情况
...第1种 空串...s第2种 只有一个\(s\)...sh第3种 有\(sh\) 但是还没有完整的\(shs\)串...shs第4种 第一个\(shs\)串...shs...s第5种 一个\(shs\)串加上一个\(s\)...shs...sh第6种 一个\(shs\)串加上一个\(sh\)...shs...shs第7种 两个\(shs\)串...shs...shs...s第8种 两个\(shs\)串加上一个\(s\)...shs...shs...sh第9种 一个\(shs\)串加上一个\(sh\)...shs...shs...shs第10种 三个\(shs\)串,此时已经可以将状态计入合法方案
这之后还有很多的状态,但是因为此题中只需要3个\(shs\)串,所以可以将剩余的状态全部归到...中去,这一步可以说就是完成了状态压缩的步骤
下面我们来考虑\(dp\)

具体转移的自动机如下(注意,之前的状态+1和这里的\(j\)对应)

但是很明显我们不可能一个个if else去转移方程
其实在写状态转移的时候就会发现有很多状态时相似的
j == 0 || j == 3 || j == 6 是几乎一样的
j == 1 || j == 4 || j == 7 是一样的
j == 2 || j == 5 || j == 8 是一样的
j == 9比较特殊。需要单独讨论
仔细讨论一下转移方程
j == 9时 后面填什么字母无所谓,所以
dp[i + 1][j] += dp[i][j] * 26; // 26个字母
j == 1 || j == 4 || j == 7 时
首先是自身\(+s\)可以转移
dp[i + 1][j] += dp[i][j];
然后是\(+h\)转移到\(j + 1\)的状态
dp[i + 1][j + 1] += dp[i][j];
最后是可以+除了\(s 、h\)以外的字母,转移至\(j - 1\)的状态
dp[i + 1][j - 1] += dp[i][j] * 24; // 还剩24个字母
剩下的\(j\)可以归结成一个
一个是\(+s\)转移至下一个状态(完整的\(shs\)和单独的\(s\))
dp[i + 1][j + 1] += dp[i][j];
还有一个就是+除了\(s\)的其他字母可以转移到\(0/3/6\)的状态\((2->0 , 5->3 , 8->6)\)
dp[i + 1][j / 3 * 3] += dp[i][j] * 25; // 还剩25个字母
时间复杂度\(O(n)\)
然后就是要注意精度 十年\(OI\)一场空 不开\(longlong\)见祖宗
C++代码
#include<bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7 , N = 1000010 , M = 10;
typedef long long LL;
LL dp[N][M] , n;
int main( )
{
cin >> n;
dp[0][0] = 1;
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < 10 ; j ++)
if(j == 9) // 最终状态
{
dp[i + 1][j] = (dp[i + 1][j] + 26 * dp[i][j]) % MOD;
}
else if(j % 3 == 1) // j == 1 || j == 4 || j == 7
{
dp[i + 1][j] = (dp[i + 1][j] + dp[i][j]) % MOD;
dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % MOD;
dp[i + 1][j - 1] = (dp[i + 1][j - 1] + 24 * dp[i][j]) % MOD;
}
else
{
dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % MOD;
dp[i + 1][j / 3 * 3] = (dp[i + 1][j / 3 * 3] + 25 * dp[i][j]) % MOD;
}
cout << dp[n][9] << '\n';
return 0;
}
二.染色游戏
题目描述
小度在公园玩一个染色游戏,染色板为一个长为\(n\),宽为 \(m\) 的长方形网格,一开始它们的颜色都是白色。
小度的颜料可以将其中的 \(k\) 个的格子染成黑色,颜料需要用完,且不能重复染色。
最终的要求是任意相邻两行或任意相邻两列要么保证完全一致,要么完全不一致。
完全一致指相邻行/列中相邻的格子要么同为白色,要么同为黑色。
完全不一致指相邻行/列中相邻的格子一个为白色,一个为黑色。
请计算有多少种染色方案。
输入格式:
一行,三个整数 \(n\),\(m\),\(k\) (\(1 \leq n , m \leq 10^7\) , \(0 \leq k \leq n×m\)) 。
输出格式:
一行,输出答案,由于答案过大所以输出答案对 \(998244353\) 取模的结果。
样例
输入:
2 2 2
输出:
6
题目分析
要点:组合数学 线性逆元
首先看题干要求,有一点很苛刻,“任意相邻两行或任意相邻两列要么保证完全一致,要么完全不一致”
这个条件就确定了如果我们从第一行开始填充之后,后面的行就是确定的了,不然完全相同,不然完全相反
所以我们的思路就明确了许多,枚举第一行可以填充的黑色块个数,这样就可以求出所有方案数
设第一行填\(i\)个黑色块 , 则与第一行完全相反的有\(m - i\)个黑色块
注意这里\(0 \le i \le \frac{m}{2}\) 是因为枚举\(\frac{m}{2}\)以上的数时其对应的相反的方案其实在之前已经计算过了
比如 在枚举\(i = 0\)时 其实已经验证并计算了\(i = m\)的可行性和方案数
再设\(n\)行中一共有y行和第一行相同 ,则有\(n - y\)行与第一行完全相反
那么一共有黑色块 \(i * y + (m - i) * (n - y)\) 个
又黑色块共有\(k\)个
所以
\(k = i * y + (m - i) * (n - y)\)
化简得
\(y = \frac{(m - i) * n - k}{-2i + m}\)
很明显 \(y\) 得是整数并且\(0 \le y \le n\)
有了\(y\)之后就是求方案数了
首先是从\(n\)行中取出\(y\)行和枚举的行相同 \(C_{n}^{y}\)
还有选取枚举行中选择涂黑的格子 \(C_{m}^{i}\)
相乘便是这种选择下的方案数
还有要注意下 \(m\)是否是偶数,是的话要看一下当正好取值\(\frac{m}{2}\)时是否正好满足,满足的话也要加进总方案数中
时间复杂度\(O(max\){\(n , m\)}\()\)
然后就是这道题目数很大,要开\(long long\)并且要用到快速幂和矩阵的线性逆元来求组合数
C++代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7 + 10 , MOD = 998244353;
int n , m , k;
int inv[N] , cm[N] , cn[N];
int qmi(int a , int b) // 快速幂
{
int res = 1;
while(b)
{
if(b & 1)
res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
void pre(int n , int m) // 预处理
{
inv[0] = inv[1] = 1;
for(int i = 2 ; i <= N ; i ++)
inv[i] = ((MOD - MOD / i) * inv[MOD % i]) % MOD; // 线性逆元
cm[0] = 1;
for(int i = 1 ; i <= m ; i ++)
cm[i] = (((cm[i - 1] * (m - i + 1)) % MOD) * inv[i]) % MOD; // cm[i] = cm[i - 1] * (m - i + 1) / i
cn[0] = 1;
for(int i = 1 ; i <= n ; i ++)
cn[i] = (((cn[i - 1] * (n - i + 1)) % MOD) * inv[i]) % MOD; // 同理
}
signed main( )
{
cin >> n >> m >> k;
if(k == 0 || k == n * m) cout << 1 << '\n';
else
{
pre(n , m);
int ans = 0;
for(int i = 0 ; i < m / 2 ; i ++)
{
int t = (m - i) * n - k , b = m - 2 * i;
if(t < 0 || t % b != 0 || t / b > n)
continue;
int y = t / b;
ans = (ans + cm[i] * cn[y]) % MOD;
}
if(!(m & 1) && (n * m / 2) == k)
ans = (ans + cm[m / 2] * qmi(2 , n - 1)) % MOD;
cout << ans << '\n';
}
return 0;
}
三.新的阶乘
题目描述
一天,小度坐在公园里学习时,灵机一动写下了这样一个运算式 \(f(x) = x^1 * (x - 1)^2 * (x - 2)^3 * ···· * 2^{(x-1)} * 1^x\) , 由于小度比较喜欢质数,他听说大数的质因子分解很难,现在小度想知道这个运算式取\(n\)时的质因子分解形式。
输入格式:
一行,一个整数 \(n ( 1 \le x \le 10^7)\),表示运算式的输入。
输出格式:
一个字符串,表示\(f(x)\)的质因子分解形式,要求按照质因子从小到大排列,当指数为\(1\)时应当忽略指数,具体格式要求参见样例。
输入:
5
输出:
f(5)=2^8*3^3*5
题目分析
思路很明确,就是把多项式因式分解
目标多项式为
\(f(x) = x^1 * (x - 1)^2 * (x - 2)^3 * ···· * 2^{(x-1)} * 1^x\)
要分解成
\(f(x) = 2^a * 3^b * 5^c * ····\)的形式
那么我们只需要把原多项式中的非质数项分解成质因子相乘的形式
首先对于质因子的筛选,我们会想到欧拉筛
const int N= 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
那么仔细想一下不难发现,我们在欧拉筛中找到非质数的方法就是将质数相乘得到合数然后在标记成\(true\)
并且经过证明这个筛法不会落下任何一个合数
这样一来就简单了许多
可以知道我们每次找到的合数的两个因子是\(primes[j]\)和 \(i\),
其中\(primes[j]\)是质数,\(i\)不一定是质数,那我们就把这两个数所对应的幂数加上该合数的幂数
但是这里筛的时候是从小开始筛,直接加上幂数的话会导致\(i\)还未分解成质因子时它的质因子就已经略过了
所以要先进行标记,记下每个合数的因子,然后再在后面从大到小累加幂数
剩下的就是输出了
记得开\(long long\)
C++代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 10000010;
typedef pair<int , int> PII;
bool st[N] , is_prime[N];
int primes[N] , cnt; // 存质数
int ans[N];
PII divide[N];
int n;
void get_euler()
{
for(int i = 2 ; i <= n ; i ++)
{
if(!st[i])
{
primes[cnt ++] = i;
is_prime[i] = true;
}
for(int j = 0 ; primes[j] <= n / i ; j ++)
{
st[primes[j] * i] = true;
divide[primes[j] * i] = {i , primes[j]}; // 先记下该合数的两个因子
if(i % primes[j] == 0) break;
}
}
for(int i = n ; i >= 2 ; i --) // 从大到小遍历
{
if(is_prime[i]) continue; // 如果本身就是质数的话不用再加一遍幂数
auto t = divide[i];
ans[t.first] += ans[i] , ans[t.second] += ans[i]; // 两个因子都要加上对应乘积合数的幂数
}
}
signed main( )
{
cin >> n;
int idx = 0;
for(int i = n ; i >= 2 ; i --) ans[i] = ++ idx;
get_euler();
cout << "f(" << n << ")=";
for(int i = 0 ; i < cnt ; i ++)
{
if(i) cout << "*";
if(ans[primes[i]] == 1) cout << primes[i] ;
else cout << primes[i] << "^" << ans[primes[i]];
}
return 0;
}
四.炼金术
题目描述
在古老的历史之中,有一个神秘的传说,那就是炼金术,这是一种可以转化为贵重金属的神秘法术。尽管大多数人都认为这只是个传说,但仍然有一些人相信它的存在,包括珠宝商,小度。
小度是一个对炼金术有着无尽痴迷的商人。他拥有两串各自由\(n\)个稀有金属珠子构成的项链,每一串项链都价值连城,然而,项链的价值并不仅仅在于它们的稀有,如果两串项链变得一摸一样将会具有对称的美丽。但受限于稀有金属的稀缺性,小度始终无法凑出足够的稀有金属获得两串一样的项链,小度始终有一个想法:如果炼金术是真的,他是否能够通过炼金术的转化,让这两串项链变得更完美,拥有对称之美呢?
于是,小度开始翻阅古书,进行了他的炼金术实验。他将自己关在工作室中,日夜不停地研究,试图找到将这对项链转化为一样的方法。然而,炼金术并非易事,他失败了无数次,但小度并没有放弃,他在古书和实验的帮助下发现了\(m\)条可以复现实验的炼金术,一项炼金术可以花费\(c_i\)小时将稀有金属\(a_i\)变成稀有金属\(b_i\) 。
然而小度的工作室条件有限,只能够同时进行一项炼金术,小度为了尽早达成夙愿他会夜以继日,他会采取最优的方法将两条项链通过炼金术获得两串一样的项链,只是项链是一个环,他不知道从哪一个位置开始会花费最小的天数,退而求其次,小度想知道他随机选择两个项链上的一个珠子开始逐一往后通过最优的策略使用炼金术的话,达成夙愿的期望小时数会是多少?
输入格式:
第\(1\)行输入两个数\(n\) \(m\) \((1 \le n \le 10^5 , 1 \le m \le 10^6)\)
接下来 \(1\) 行,\(n\)个数\(s_1 , s_2 , ··· , s_n\) 表示第一条项链从左到右稀有金属的种类;
接下来 \(1\) 行,\(n\)个数\(t_1 , t_2 , ··· , t_n\) 表示第二条项链从左到右稀有金属的种类;
接下来\(m\)行,每行三个数 \(a_i b_i c_i\) ,含义与题目描述一致,\((1 \le s_i , t_i , a_i , b_i \le 400 , 1 \le c_i \le 100)\)
可能存在多个将 \(a_i\) 变为 \(b_i\) 的炼金术,保证所有金属可以互相转换。
输出格式:
一行,一个保留\(2\)位小数的浮点数,表示期望的小时数。
样例输入:
3 14
4 5 3
2 4 3
3 2 65
2 3 38
4 2 55
2 4 95
4 3 88
3 4 58
5 2 84
2 5 24
5 3 23
3 5 97
5 4 25
4 5 23
4 5 33
1 2 100
样例输出:
82.33
题目分析
刚开始以为很简单,直接就写了,然后就\(TLE\)了
看题目很容易看出这道题考的是多源汇最短路,直接就用\(Floyd\)做,
\(Floyd\)时间复杂度\(O({cnt}^3)\) 约等于 \(7 * 10^7\)
本以为是能过的,结果后来发现要\(n^2\) 然后就挂了
C++ 代码1:
未优化
\((7 / 11) TLE\) 做法 时间复杂度\(O(n^2 * cnt)\)
#include<bits/stdc++.h>
using namespace std;
const int N = 100010 , M = 410;
int dist[M][M];
int s[N] , t[N];
int n , m;
int cnt = 0;
void floyd()
{
for (int k = 1; k <= cnt; k ++ )
for (int i = 1; i <= cnt; i ++ )
for (int j = 1; j <= cnt; j ++ )
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
int main( )
{
cin >> n >> m;
for(int i = 0 ; i < n ; i ++) cin >> s[i];
for(int i = 0 ; i < n ; i ++) cin >> t[i];
for(int i = 1 ; i < M ; i ++)
for(int j = 1 ; j < M ; j ++)
if(i == j) dist[i][j] = 0;
else dist[i][j] = 0x3f3f3f3f;
while(m --)
{
int a , b , c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b] , c);
cnt = max(cnt , max(b , a));
}
floyd();
long long res = 0ll;
for(int i = 0 ; i < n ; i ++)
{
long long now = 0;
for(int j = 0 ; j < n ; j ++)
{
int sc = s[j] , tc = t[(i + j) % n];
int cost = 0x3f3f3f3f;
for(int k = 1 ; k <= cnt ; k ++)
cost = min(cost , dist[sc][k] + dist[tc][k]);
res += cost;
}
}
long double ans = 1. * res / n;
printf("%.2Lf" , ans);
return 0;
}
接下来就是优化了
其实就是把\(n\)缩小,看数据可以知道其实\(s\)、\(t\)中大部分都是重复字符,所以用map来优化
而且在每次循环中可以发现
for(int k = 1 ; k <= cnt ; k ++)
cost = min(cost , dist[sc][k] + dist[tc][k]);
这个操作其实可以预处理,用单独的数组存一下任意两个字符选择变成那个字符最优即可
C++代码 时间复杂度\(O(max (log^2n, cnt^3))\)
#include<bits/stdc++.h>
using namespace std;
const int N = 100010 , M = 410;
int dist[M][M];
int g[M][M];
int s[N] , t[N];
map<int , int> q1 , q2;
int n , m;
int cnt = 0;
void floyd()
{
for (int k = 1; k <= cnt; k ++ )
for (int i = 1; i <= cnt; i ++ )
for (int j = 1; j <= cnt; j ++ )
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
void cal()
{
for (int k = 1; k <= cnt; k ++ )
for (int i = 1; i <= cnt; i ++ )
for (int j = 1; j <= cnt; j ++ )
if(i == j) g[i][j] = 0;
else g[i][j] = min(g[i][j] , dist[i][k] + dist[j][k]);
}
int main( )
{
cin >> n >> m;
for(int i = 0 ; i < n ; i ++)
{
cin >> s[i];
q1[s[i]] ++;
}
for(int i = 0 ; i < n ; i ++)
{
cin >> t[i];
q2[t[i]] ++;
}
for(int i = 1 ; i < M ; i ++)
for(int j = 1 ; j < M ; j ++)
if(i == j) dist[i][j] = 0;
else dist[i][j] = 0x3f3f3f3f;
while(m --)
{
int a , b , c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b] , c);
cnt = max(cnt , max(b , a));
}
floyd();
memset(g , 0x3f , sizeof g);
cal();
long long res = 0ll;
for(auto [k1 , v1] : q1)
for(auto [k2 , v2] : q2)
res += (long long) v1 * v2 * g[k1][k2];
long double ans = 1. * res / n;
printf("%.2Lf" , ans);
return 0;
}
五.新材料
题目描述
小度最近在研究超导材料,他合成了 \(N\) 个新材料排成一排,而每个材料根据基于流程会设置一个编号\(P\),表示种类。 小度发现相同种类的材料在距离小于等于\(K\) 的情况下会发生反应。
小度想知道哪些种类的材料会发生反应,输出这些种类 \(P\) 的异或值。
输入格式:
第一行,两个整数 \(N , K (1 \le N , K \le 50000)\)
接下来 \(N\) 行,每行一个整数,即该位置材料的的种类编号 \(P (1 \le P \le 1000000)\)
输出格式:
一行,一个整数,会发生反应的种类\(P\) 的异或值。
样例
输入:
5 3
1
2
4
1
4
输出:
5
题目分析
模拟题,没什么好说的,也不用哈希表,\(O(n)\)过得去
C++ 代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000010;
int d[N];
bool st[N];
int n , k , p;
signed main( )
{
cin >> n >> k;
for(int i = 1 ; i <= n ; i ++)
{
int x;
cin >> x;
if(d[x] && i - d[x]<= k)
if(!st[x])
{
p ^= x;
st[x] = true;
}
d[x] = i;
}
cout << p << '\n';
return 0;
}
Go 代码
package main
import "fmt"
func main() {
var a , st [1000010]int64
var n , k , p , i int64
fmt.Scanf("%d %d\n" , &n , &k)
for i = 1 ; i <= n ; i ++{
var x int64
fmt.Scanf("%d\n" , &x)
if a[x] == 0 {
a[x] = i
} else {
if i - a[x] <= k && st[x] == 0 {
p ^= x
st[x] = 1
} else {
a[x] = i
}
}
}
fmt.Printf("%d" , p)
}
六.魔法阵
题目描述
在远古的神秘土地上,有一片神奇的区域(可以视为二维平面),其中分布着许多神秘的魔力点。
小度是一个远近闻名的魔术师,他的魔法水平可以用一个整数\(M\)表示。
当小度在区域中的某个位置施法时,若一个魔力点与小度的欧几里得距离的平方不超过\(M\),则这个魔力点可以为小度提供魔力。
目前区域内有\(n\)个魔力点,接下来\(m\)天每天都会发生一个事件,事件可以分成两类:
\(add \quad x \quad y:\)在坐标\((x,y)\)出现了一个新的魔力点。
\(query \quad x \quad y:\)小度在坐标\((x,y)\)施法。
小度希望每次施法的时候,所有存在的魔力点都能为他提供魔力。但是他目前的魔术水平不一定能够做到。
小度希望知道他的魔法水平\(M\)至少要达到多少,才能做到每次施法都能让所有存在的魔力点为他提供魔力。
输入格式:
第一行包含两个正整数 \(n\) 和 \((1≤ n,m ≤10^5 )\), 分别表示初始点集合的大小和天数;
接下来\(n\)行,每行包含两个整数\(x\)和\(y\) \((−10^9 ≤ x,y ≤ 10^9 )\),表示集合中的一个魔力点的横坐标和纵坐标;
再接下来\(m\)行,每行描述一个操作。
操作描述以字符串\(s\)开头,若\(s\)为\(“add”\),表示添加点操作,后跟两个整数\(x\)和\(y\) \((−10^9≤x,y≤10^9)\),表示要添加的魔力点的横坐标和纵坐标;若\(s\)为\(“query”\),表示查询点操作(施法),后跟两个整数\(x\)和\(y\) \((−10^9 ≤x,y≤10^9)\),表示需要查询的点的横坐标和纵坐标。
输出格式:
一行包含一个整数,输出答案。
输入:
5 3
0 0
3 4
1 1
-2 7
6 -8
query 2 3
add 5 -5
query 0 0
输出:
137
题目分析
凸包+闵可夫斯基和+分治
题目还是有难度的
先说一下做题历程吧
刚开始肯定是暴力,纯暴力\(O(n * m)\),当然也只过了4个点
C++ 代码
#include<bits/stdc++.h>
#define int long long
#define x first
#define y second
using namespace std;
typedef pair<int , int> PII;
const int N = 200010;
int n , m , idx ;
string s;
PII q[N];
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
cin >> n >> m;
idx = n;
for(int i = 0 ; i < n ; i ++)
cin >> q[i].x >> q[i].y;
sort(q , q + n , [&](PII a , PII b){
return a.x * a.x + a.y * a.y < b.x * b.x + b.y * b.y;
});
int a , b;
int ans;
while(m --)
{
cin >> s >> a >> b;
if(s == "add")
{
q[idx ++] = {a , b};
sort(q , q + idx , [&](PII a , PII b){
return a.x * a.x + a.y * a.y < b.x * b.x + b.y * b.y;
});
}
else
{
auto t = q[idx - 1] , k = q[0];
ans = max(ans , max((t.x - a) * (t.x - a) + (t.y - b) * (t.y - b) , (k.x - a) * (k.x - a) + (k.y - b) * (k.y - b)));
}
}
cout << ans << '\n';
return 0;
}
下面来讲讲看了题解的思路
从上面的暴力可以看出,在线做法肯定是行不通的,每次增加和询问都得\(O(n)\) 肯定是会爆掉的
所以采用离线做法
首先看题目很好理解,就是就点集的最长距离,再细一点,点集一共有两种,一种是原来就给定的\(n\)个点的点集,然后就是新增询问中的\(m\)个点的点集,而且这个点集中还要注意,有的是增点,有的是询问,我们要求的询问的点与再此之前的点集的最远距离
图示如下

要求红点点集与蓝点和黑点点集的最长距离,同时还要注意求的先后顺序,因为只有在询问距离之前新增的点对最长距离有贡献
为了方便,我们先暂且忽略先后贡献的问题,我们先解决点集之间最长距离的事
我们发现。点集之间最长距离一定不会由中间的点决定,那么对于两个点集,我们先求对应凸包

求这两个凸包之间的最长距离,暴力就得\(O(n * m)\),肯定不行
那就要把这两个凸包按照某个顺序选择点进行计算,优化成线性复杂度\(O(n)\)
这个时候我们想到了闵可夫斯基和
闵可夫斯基和通俗点讲就是将一个图形\(A\)平移地放在着图形\(B\)每个顶点上,再将此时图形\(A\)的顶点标注出来,求出此时的凸包,这就是闵可夫斯基和了
根据对称性,\(A\)放\(B\)上和\(B\)放\(A\)上构成的凸包应该是相同的,所以闵可夫斯基和是唯一的
举个三角形和四边形的闵可夫斯基和的例子(图是盗的)

再仔细看看图形,会感觉好像跟题目要求的距离没啥关系
题目要求两个点集之间的最远距离
形象一点 有两个点\(P(x1 , y1)\)和\(Q(x2 , y2)\)
那我们就要求 \(P - Q = (x1 - x2 , y1 - y2)\) 向量的模长最大值
欸,等一下,向量模长最大值,每个向量对应一条边,如果这些向量能构成凸包就好了,这样就能和求出来的闵可夫斯基和挂上钩了
再想想,怎么才能求出这样的闵可夫斯基和呢,其实把点\(Q\)所在点集都换成和\(-Q\)值一样的点不就行了,这样求出来的闵可夫斯基和每条边就和要求的向量对应了
这样就可以转化成求闵可夫斯基和凸包边模长的最大值
闵可夫斯基和求法如下:
求凸包之间的闵可夫斯基和的方法。
把两个凸包的每一条向量都抠出来,按照极角序排序构成新凸包即可。
注意点和向量的去重(向量相同斜率去重)。
还有个地方可以提一下:求多个凸包的闵可夫斯基和的时候可以直接全把边拿出来一块求,没有必要两个两个求。
具体实现的时候,找出最高且最靠左的点。
先把这个点加入答案,从这个点开始把所有向量遍历一遍,最后去掉最后一个点即可(最后这个点会和第一个点重合)。
现在求最长距离的事情解决了,还有个事就是得考虑下时间的问题,本来想的kd树或时间序列啥的,但其实很简单
分治就行了
神奇吧
仔细想想,对于询问距离的所有点来说,只有在它之前的点对其距离的最大值有贡献,那我们就分成前半段和后半段
前半段的存在的点和所有新增的点都可以对后半段的询问距离做出贡献,那我们就求前半段存在的点和所有新增的点(蓝+黑)所构成的点集和后半段询问距离的点(红)构成的点集求闵可夫斯基和,然后继续递归分治即可
最后给下代码 但是还是有一个点被卡
C++代码 (34/35) 时间复杂度\(O(nlog^2(n))\)
#include<bits/stdc++.h>
#define x first
#define y second
#define int long long
using namespace std;
typedef pair<int , int> PII;
const int N = 200010;
PII q[N];
int n , m , op[N] ;
int ans;
PII operator+(PII a ,PII b)
{
return {a.x + b.x , a.y + b.y};
}
PII operator-(PII a , PII b)
{
return {a.x - b.x , a.y - b.y};
}
int operator*(PII a , PII b)
{
return a.x * b.y - a.y *b.x;
}
int operator&(PII a , PII b)
{
return a.x * b.x + a.y *b.y;
}
vector<PII> andrew(vector<PII> q)
{
for(auto &i : q)
if(i.x == q[0].x && i.y < q[0].y || i.x < q[0].x) swap(i , q[0]);
sort(q.begin() + 1 , q.end() , [&](PII a , PII b){
return ((a - q[0]) * (b - q[0])) > 0;
});
vector<PII> ans;
for(auto i : q)
{
while(ans.size() >= 2 && (i - ans.back()) * (ans.back() - ans[ans.size() - 2]) >= 0 )
{
ans.pop_back();
}
ans.push_back(i);
}
return ans;
}
int cmp(PII a , PII b)
{
if((a.x > 0 || a.x == 0 && a.y > 0) != (b.x > 0 || b.x == 0 && b.y > 0)) return (a.x > 0 || a.x == 0 && a.y > 0);
return a * b > 0;
}
void Mincowsky_sum(vector<PII> a , vector<PII> b) // 闵可夫斯基和
{
if(a.empty() || b.empty()) return;
a = andrew(a) , b = andrew(b);
vector<PII> c , ca , cb;
if(a.size() > 1)
{
a.push_back(a.front());
for(int i = 1 ; i < a.size() ; i ++) ca.push_back(a[i] - a[i - 1]);
}
if(b.size() > 1)
{
b.push_back(b.front());
for(int i = 1 ; i < b.size() ; i ++) cb.push_back(b[i] - b[i - 1]);
}
c.resize(ca.size() + cb.size());
merge(ca.begin() , ca.end() , cb.begin() , cb.end() , c.begin() , cmp);
PII cur = a[0] + b[0];
ans = max(ans , (cur & cur));
for(auto i : c)
cur = cur + i , ans = max(ans ,(cur & cur));
}
void calc(int l , int r) // 分治
{
if(l == r) return;
int mid = l + r >> 1;
vector<PII> L , R;
for(int i = l ; i <= mid ; i ++) if(op[i] == 1) L.push_back(q[i]);
for(int i = mid + 1 ; i <= r ; i ++) if(op[i] == 2) R.push_back(q[i]);
Mincowsky_sum(L , R);
calc(l , mid) , calc(mid + 1 , r);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++) cin >> q[i].x >> q[i].y , op[i] = 1;
for(int i = n + 1 ; i <= n + m ; i ++)
{
string s;
cin >> s >> q[i].x >> q[i].y;
if(s == "add") op[i] = 1;
else op[i] = 2 , q[i].x = -q[i].x , q[i].y = -q[i].y;
}
calc(1 , n + m);
cout << ans << '\n';
return 0;
}
七.竞猜
题目描述
有\(n\) 支队伍参加一项赛事,其中\(n=2^k ,k∈N\) ,给这些队伍从 \(1\) 到 $n $编号。
该赛事采用淘汰赛制,经过\(k\) 轮共\(2^k − 1\) 场淘汰赛决出最后的冠军。具体规则如下:
- 第 \(1\) 轮比赛有 \(2^k − 1\) 场,分别是\(1\)号队伍对阵\(2\)号队伍,\(3\) 号队伍对阵\(4\) 号队伍,\(5\) 号队伍对阵 \(6\) 号队伍,......... ,\(2^k − 1\) 号队伍对阵\(2^k\) 号队伍。
- 对于所有 \(2 \le t \le k\),第\(t\) 轮比赛有\(2^{k−t}\) 场,分别是第\(t−1\) 轮第\(1\)场的胜者对阵第\(t−1\) 轮第\(2\) 场的胜者,第\(t−1\) 轮第\(3\) 场的胜者对阵第\(t−1\) 轮第\(4\) 场的胜者,第\(t−1\) 轮第\(5\)场的胜者对阵第\(t−1\) 轮第\(6\) 场的胜者,…,第\(t−1\) 轮第\(2^{k−t+1} − 1\) 场的胜者对阵第\(t−1\) 轮第\(2^{k−t+1}\) 场的胜者。
某公司给出一个竞猜规则:如果在第\(i\)支队伍身上下\(x\)的赌注,则该队伍每参加一场比赛,返还\(x\)元,如果该队伍最终夺冠,则额外奖励 \(a_ix\) 元,其中\(a_i\) 为该公司在整届赛事开始前给出的夺冠赔率。
现有一个赌怪想要参加竞猜,他一开始打算给第\(i\) 支队伍下\(b_i\) 赌注。他会进行\(q\) 次操作,每次操作给定\(id\) 和\(d\),表示将\(b_{id}\) 改为\(b_{id} + d\),然后询问他至少可以获得多少回报。
输入格式:
第 \(1\) 行输入 \(1\) 个正整数\(n(n=2^k ,1 \le k \le 20)\),表示有\(n\) 支队伍参加该项赛事;
第 \(2\) 行输入\(n\) 个正整数\(a_i (1 \le a_i \le 100)\),分别表示第\(i\) 支队伍的夺冠赔率;
第 \(3\) 行输入\(n\) 个非负整数 \(b_i (0 \le b_i \le 10^5)\) ,分别表示一开始赌怪在第\(i\) 支队伍身上下的赌注;
第 \(4\) 行输入\(1\) 个整数\(q(1 \le q \le 5×10^5)\),表示共有\(q\) 次操作;
接下来共有\(q\) 行,每行输入\(2\) 个数,其中的第\(i\) 行输入两个整数\(id_i(1 \le id_i \le n)\) 和\(d_i (−100 \le d_i
\le 100)\),表示将\(b_{id_i}\) 修改为 \(b_{id_i} + d_i\) 保证任意修改后\(0 \le b_{id_i} \le 10^5\)
输出格式:
\(q\) 行,每行输出一个整数,第 \(i\) 行输出第\(i\)次操作后赌怪至少可以获得的回报。
样例1
输入:
2
1 6
7 2
1
2 7
输出:
23
样例 2
输入:
4
8 3 5 1
7 5 9 9
2
1 8
4 -2
输出:
61
55
题目分析
线段树+贪心
首先画一颗比赛树

我们要计算所有比赛队伍能够带来的最小收益(因为问的是至少)
再观察题目,可以知道夺冠队伍对总收益的贡献和非夺冠队伍的贡献不一样
对于非夺冠队伍贡献值的计算,只要沿着它获胜的路径累加一遍即可
这样一来,我们可以发现,其计算的本质有点像\(DFS\),但是深搜一遍肯定会\(TLE\),又所有的节点操作又是一样的,我们不难想到线段树这种数据结构
那么节点又应该有什么性质呢?
首先应该有\(l,r\) 线段树基本性质
然后应该去维护一下这个节点是哪个子节点能够胜利,看哪个子节点的价值小就行
最后还要看这个节点对总收益的贡献,而且注意,获胜和未获胜的贡献还是不一样的
struct TreeNode
{
int l , r;
LL val;
LL w[2]; // w[1] 表示夺冠的贡献 w[0] 表示非夺冠的贡献
};
现在考虑维护信息
初始建树的时候
对于子节点\(val\)和\(w[0]\)是一样的应该都是他们所对应的赌注
\(w[1]\)不同,看题目,获胜的队伍获胜的时候会获得\(a_ix\)的奖励所以对于叶子节点,如果是获胜队伍的话贡献应该是本身的赌注乘以到根节点儿子的深度再加上本身夫人赌注乘以夺冠倍率
对于更新父节点信息
本身的节点胜利的是谁,价值就是哪个子节点的价值,根据贪心,我们要选择价值小的子节点
tr[u].val = min(tr[u << 1].val , tr[u << 1 | 1].val);
然后是计算贡献
如果未夺冠,那么该节点的总贡献就是本身胜利的队伍赌注加上两个子节点的总贡献,因为是未获胜,所以加上的自然也是子节点的未获胜状态的贡献
tr[u].w[0] = tr[u << 1].w[0] + tr[u << 1 | 1].w[0] + tr[u].val;
如果夺冠,那么该节点的总贡献就会不同,首先本身胜利的队伍就是夺冠的队伍,之前给子节点赋值的时候讨论过,获胜叶子节点的贡献值是直接计算到根的,不需要再计算一遍,所以不需要再加一遍节点的本身价值,现在就只需要考虑子节点到父节点贡献值的状态转移了
夺冠的只能有一个子节点,另一个子节点必须未获胜,所以一共就两种情况,贪心取最小即可
tr[u].w[1] = min(tr[u << 1].w[0] + tr[u << 1 | 1].w[1] , tr[u << 1].w[1] + tr[u << 1 | 1].w[0]);
剩下的就简单了,修改就是板子中再简单不过的单点查询,总贡献的最小值就是根节点的贡献值,注意,根节点必须获胜(不然哪来的冠军),代码如下
C++ 代码 MLE(10/20)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = (1 << 20) + 10;
int n , k ;
int a[N] , b[N];
struct TreeNode
{
int l , r;
LL val;
LL w[2]; // w[1] 表示夺冠的贡献 w[0] 表示非夺冠的贡献
}tr[N * 4];
void pushup(int u)
{
tr[u].val = min(tr[u << 1].val , tr[u << 1 | 1].val);
tr[u].w[0] = tr[u << 1].w[0] + tr[u << 1 | 1].w[0] + tr[u].val;
tr[u].w[1] = min(tr[u << 1].w[0] + tr[u << 1 | 1].w[1] , tr[u << 1].w[1] + tr[u << 1 | 1].w[0]);
}
void build(int u , int l , int r)
{
tr[u].l = l , tr[u].r = r;
if(l == r)
{
tr[u].w[0] = tr[u].val = b[l];
tr[u].w[1] = (k - 1 + a[l]) * b[l];
return;
}
int mid = l + r >> 1;
build(u << 1 , l , mid);
build(u << 1 | 1 , mid + 1 , r);
pushup(u);
}
void modify(int u , int id , int d)
{
if(tr[u].l == id && tr[u].r == id) // 找到了
{
tr[u].val += d;
tr[u].w[0] += d;
tr[u].w[1] += (k - 1 + a[tr[u].l]) * d;
b[tr[u].l] += d;
return;
}
int mid = tr[u].l + tr[u].r >> 1;
if(id <= mid) modify(u << 1 , id , d);
else modify(u << 1 | 1 , id , d);
pushup(u);
}
int main( )
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
cin >> n;
while(1 << k != n) k ++;
k ++;
for(int i = 1 ; i <= n ; i ++) cin >> a[i];
for(int i = 1 ; i <= n ; i ++) cin >> b[i];
build(1 , 1 , n);
int q;
cin >> q;
while(q --)
{
int id , d;
cin >> id >> d;
modify(1 , id , d);
cout << tr[1].w[1] << '\n';
}
return 0;
}
交完后发现\(MLE\)了,算一下,结构体中两个\(int\),两个\(long \quad long\) ,就是\(24\)字节,\(2^{20} * 4 * 24 \approx 2^{25}\)
题目是\(64MB\) 就是\(64 * 1024 * 1024 = 2^{26}\) 很接近,所以爆了
所以需要将节点中的\(l , r\) 优化掉,因为只在查询中用过\(l , r\),所以我们直接折半查询,缩短查询范围而不是通过节点本身的\(l , r\)来确定查询范围
代码如下
C++ 代码 AC(20/20)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = (1 << 20) + 10;
int n , k ;
int a[N] , b[N];
struct TreeNode
{
LL val;
LL w[2]; // w[1] 表示夺冠的贡献 w[0] 表示非夺冠的贡献
}tr[N * 4];
void pushup(int u)
{
tr[u].val = min(tr[u << 1].val , tr[u << 1 | 1].val);
tr[u].w[0] = tr[u << 1].w[0] + tr[u << 1 | 1].w[0] + tr[u].val;
tr[u].w[1] = min(tr[u << 1].w[0] + tr[u << 1 | 1].w[1] , tr[u << 1].w[1] + tr[u << 1 | 1].w[0]);
}
void build(int u , int l , int r)
{
if(l == r)
{
tr[u].w[0] = tr[u].val = b[l];
tr[u].w[1] = (k - 1 + a[l]) * b[l];
return;
}
int mid = l + r >> 1;
build(u << 1 , l , mid);
build(u << 1 | 1 , mid + 1 , r);
pushup(u);
}
void modify(int u , int l , int r , int id , int d)
{
if(l == id && r == id) // 找到了
{
tr[u].val += d;
tr[u].w[0] += d;
tr[u].w[1] += (k - 1 + a[l]) * d;
b[l] += d;
return;
}
int mid = l + r >> 1;
if(id <= mid) modify(u << 1 , l , mid , id , d);
else modify(u << 1 | 1 , mid + 1 , r , id , d);
pushup(u);
}
int main( )
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
cin >> n;
while(1 << k != n) k ++;
k ++;
for(int i = 1 ; i <= n ; i ++) cin >> a[i];
for(int i = 1 ; i <= n ; i ++) cin >> b[i];
build(1 , 1 , n);
int q;
cin >> q;
while(q --)
{
int id , d;
cin >> id >> d;
modify(1 , 1 , n , id , d);
cout << tr[1].w[1] << '\n';
}
return 0;
}
以上是这次百度之星的所有题目解答,有问题欢迎评论区讨论指正

2023百度之星 所有题解
浙公网安备 33010602011771号