高质量小测题,值得一做
做题的时候,一定要认真读题,不要像这个人一样
A. Coprime
题面
有 N N N 个整数,第 i i i 个数为 A i A_i Ai。
当对于所有
1
≤
i
<
j
≤
N
1 \leq i < j \leq N
1≤i<j≤N ,都有
G
C
D
(
A
i
,
A
j
)
GCD(A_i,A_j)
GCD(Ai,Aj) 时,称
{
A
i
}
\{A_i\}
{Ai} 是 pairwise coprime
(两两互质)的。
当
{
A
i
}
\{A_i\}
{Ai} 不是 pairwise coprime
,但
G
C
D
(
A
1
,
…
,
A
N
)
=
1
GCD(A_1, \ldots, A_N) = 1
GCD(A1,…,AN)=1 时,称
{
A
i
}
\{A_i\}
{Ai} 是 setwise coprime
(集合互质)的。
请判断
{
A
i
}
\{A_i\}
{Ai} 是 pairwise coprime
、setwise coprime
,还是都不是。
其中 G C D ( … ) GCD(\ldots) GCD(…) 表示最大公约数。
思路
很明显,这道题暴力做肯定会超时。所以我们有如下思路:
计算整体 GCD:遍历所有数,计算它们的整体 GCD。
检查两两互质:统计每个质数在数组中出现的次数。如果所有质数的出现次数都不超过 1,则说明两两互质。
判断结果:
- 如果两两互质,输出
pairwise coprime
。 - 如果不是两两互质但整体 GCD 为 1,输出
setwise coprime
。 - 否则,输出
not coprime
。
现在,我们就得到了一个满分思路,我可真强
由于代码过于简单,就不做过多说明了
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int spf[N];
int prime_cnt[N],a[N],n,overall_gcd;
void sieve(){
for(int i=0;i<N;i++) spf[i]=i;
for(int i=2;i*i<N;i++)
if(spf[i]==i)
for(int j=i*i;j<N;j+=i)
if(spf[j]==j) spf[j]=i;
}
vector<int> get_primes(int x){
vector<int> primes;
while(x>1){
int p=spf[x];
primes.push_back(p);
while(x%p==0) x/=p;
}
return primes;
}
int main(){
sieve();
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
overall_gcd=a[1];
for(int i=2;i<=n;i++) overall_gcd=__gcd(overall_gcd,a[i]);
bool is_pairwise=true;
for(int i=1;i<=n;i++){
vector<int> primes=get_primes(a[i]);
for(int p:primes){
if(prime_cnt[p]>=1) is_pairwise=false;
prime_cnt[p]++;
}
}
if(is_pairwise) cout<<"pairwise coprime";
else if(overall_gcd==1) cout<<"setwise coprime";
else cout<<"not coprime";
return 0;
}
B. 旅游
题面
小杨准备前往 B 国旅游。
B 国有 n n n 座城市,这 n n n 座城市依次以 1 1 1 至 n n n 编号。城市之间由 n − 1 n-1 n−1 条双向道路连接,任意两座城市之间均可达(即任意两座城市之间存在可达的路径)。
小杨可以通过双向道路在城市之间移动,通过一条双向道路需要 1 1 1 单位时间。
B 国城市中有 k k k 座城市设有传送门。设有传送门的城市的编号依次为 b 1 , b 2 , ⋯ , b k b_1,b_2, \cdots ,b_k b1,b2,⋯,bk。小杨可以从任意一座设有传送门的城市花费 0 0 0 单位时间前往另一座设有传送门的城市。
注:如果两座设有传送门的城市之间存在双向道路,那么小杨可以选择通过双向道路移动,也可以选择通过传送门传送。
小杨计划在 B 国旅游 q q q 次。第 i i i 次旅游( 1 ≤ i ≤ q 1 \le i \le q 1≤i≤q),?杨计划从编号为 u i u_i ui 的城市前往编号为 v i v_i vi 的城市,小杨希望你能求出所需要的最短时间。
思路
比较明显的 LCA 模板(不懂的可以上我的博客,里面详细介绍了关于LCA)
具体思路:
- 树的表示:使用邻接表来存储树结构。
- LCA预处理:使用二进制提升法预处理每个节点的祖先信息,以便快速查询任意两点的LCA。
- 计算树上距离:对于任意两点u和v,它们之间的距离可以通过公式
depth[u] + depth[v] - 2 * depth[lca(u, v)]
计算。 - 传送门处理:
- 使用多源BFS预处理每个节点到最近的传送门节点的距离。
- 对于每次查询,最短路径可能是直接走树上的路径,或者通过最近的传送门进行传送。
- 查询处理:对于每个查询(u, v),计算以下两种情况的最小值:
- 直接走树上的路径:
distance(u, v)
- 通过传送门:
distance(u, nearest_portal[u]) + distance(nearest_portal[v], v)
- 直接走树上的路径:
代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5; // 最大节点数
const int L=20; // LCA的二进制提升深度
// 图的邻接表存储
int g[N], to[N*2], nxt[N*2], cnt;
// 节点信息
int d[N]; // 节点深度
int f[N][L]; // 二进制提升表
int np[N]; // 是否是传送门
int dis[N]; // 到最近传送门的距离
int near[N]; // 最近传送门节点
// BFS队列
int q[N], hd, tl; // 队列和头尾指针
// 传送门列表
int pt[N], k; // 传送门数组和数量
// 添加边
void add(int u, int v) {
to[++cnt] = v;
nxt[cnt] = g[u];
g[u] = cnt;
}
// DFS预处理LCA
void dfs(int u, int fa) {
f[u][0] = fa; // 直接父节点
// 二进制提升预处理
for(int i=1; i<L; ++i)
f[u][i] = f[f[u][i-1]][i-1];
// 遍历子节点
for(int i=g[u]; i; i=nxt[i]) {
int v = to[i];
if(v != fa) {
d[v] = d[u] + 1; // 计算深度
dfs(v, u);
}
}
}
// 求LCA
int lca(int u, int v) {
// 使u成为较深的节点
if(d[u] < d[v]) swap(u, v);
// u向上跳到与v同深度
for(int i=L-1; i>=0; --i)
if(d[u] - (1<<i) >= d[v])
u = f[u][i];
if(u == v) return u;
// 同时向上跳
for(int i=L-1; i>=0; --i)
if(f[u][i] != f[v][i]) {
u = f[u][i];
v = f[v][i];
}
return f[u][0];
}
// 计算树上两点距离
int dist(int u, int v) {
return d[u] + d[v] - 2*d[lca(u,v)];
}
// 多源BFS预处理最近传送门
void bfs(int n) {
hd = 1, tl = 0;
memset(dis, -1, sizeof dis);
// 初始化传送门节点
for(int i=1; i<=k; ++i) {
dis[pt[i]] = 0;
near[pt[i]] = pt[i];
q[++tl] = pt[i];
}
// BFS
while(hd <= tl) {
int u = q[hd++];
// 遍历邻接节点
for(int i=g[u]; i; i=nxt[i]) {
int v = to[i];
if(dis[v] == -1) {
dis[v] = dis[u] + 1;
near[v] = near[u];
q[++tl] = v;
}
}
}
}
int main() {
int n, m, q;
cin >> n >> k >> q;
// 建树
for(int i=1; i<n; ++i) {
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
}
// 读传送门
for(int i=1; i<=k; ++i) {
cin >> pt[i];
np[pt[i]] = 1;
}
// 预处理LCA
dfs(1, 1);
// 预处理最近传送门
if(k) bfs(n);
// 处理查询
while(q--) {
int u, v;
cin >> u >> v;
int ans = dist(u, v); // 直接走的距离
if(k) ans = min(ans, dis[u] + dis[v]); // 通过传送门的距离
cout << ans << endl;
}
return 0;
}
学好LCA。
不听我之言,吃亏在眼前。
C. 考试
题面
有 n n n 位同学,每位同学都参加了全部的 m m m 门课程的期末考试,都在焦急的等待成绩的公布。
第 i i i 位同学希望在第 t i t_i ti 天或之前得知所有课程的成绩。如果在第 t i t_i ti 天,有至少一门课程的成绩没有公布,他就会等待最后公布成绩的课程公布成绩,每等待一天就会产生 C C C 不愉快度。
对于第 i i i 门课程,按照原本的计划,会在第 b i b_i bi 天公布成绩。
有如下两种操作可以调整公布成绩的时间:
- 将负责课程 X X X 的部分老师调整到课程 Y Y Y,调整之后公布课程 X X X 成绩的时间推迟一天,公布课程 Y Y Y 成绩的时间提前一天;每次操作产生 A A A 不愉快度。
- 增加一部分老师负责学科 Z Z Z,这将导致学科 Z Z Z 的出成绩时间提前一天;每次操作产生 B B B 不愉快度。
上面两种操作中的参数 X , Y , Z X, Y, Z X,Y,Z 均可任意指定,每种操作均可以执行多次,每次执行时都可以重新指定参数。
现在希望你通过合理的操作,使得最后总的不愉快度之和最小,输出最小的不愉快度之和即可。
思路
一道很好的三分题
但是我竟然写挂了
确定目标时间T的范围:T的可能取值范围是所有学生的 t i t_i ti 和所有课程的 b i b_i bi 的并集,以及可能的中间值。
计算操作成本:对于给定的 T,计算将所有课程调整到不超过 T 的时间所需的最小操作成本。这包括:
- 使用操作1(调整老师)和操作2(增加老师)来提前课程的公布时间。
计算学生不愉快度:对于给定的 T,计算所有学生的不愉快度,即对于每个学生,如果 t i < T t_i < T ti<T,则产生 C × ( T − t i ) C\times(T - t_i) C×(T−ti) 的不愉快度。
寻找最优T:使用三分法在所有可能的T中寻找使总不愉快度最小的那个T,因为总不愉快度关于T的函数通常是单峰的或有一个最小值。
代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N=1e5+5;
ull a,b,c,n,m,t[N],bi[N],ans=1ull<<60;
ull cal(ull T){
ull cst=0;
ull s1=0,s2=0;
for(int i=1;i<=m;i++)
if(bi[i]>T) s1+=bi[i]-T; // 需要提前的天数
else s2+=T-bi[i]; // 可以推迟的天数
if(a<b){
ull move=min(s1,s2);
cst+=a*move;
s1-=move;
}
cst+=b*s1; // 剩余天数用操作2
// 计算学生的不愉快度
for(int i=1;i<=n;i++)
if(t[i]<T)
cst+=(T-t[i])*c;
return cst; // 返回总不愉快度
}
int main(){
cin>>a>>b>>c>>n>>m;
for(int i=1;i<=n;i++) cin>>t[i];
for(int i=1;i<=m;i++) cin>>bi[i];
// 确定三分搜索的初始范围
ull l=1;
ull r=max(*max_element(t+1,t+n+1),
*max_element(bi+1,bi+m+1));
// 三分搜索寻找最优解
while(l<=r){
ull m1=l+(r-l)/3;
ull m2=r-(r-l)/3;
ull c1=cal(m1); // 计算m1的总不愉快度
ull c2=cal(m2); // 计算m2的总不愉快度
ans=min(ans,min(c1,c2)); // 更新最小值
if(c1<c2) r=m2-1;
else l=m1+1;
}
cout<<ans;
return 0;
}
噢,忘了说了
这里有个小细节,我考试时开了 long long
俗话说不开long long
见祖宗
但是还是有一个点 WA:
调了半天
才发现要开unsigned long long
所以这句话应该这么说:
不开unsigned long long
见祖宗
D. 寻宝
本蒟蒻找不到原题,将就看着题面吧
题面
kkkw意外的获得了一张神奇的藏宝图,但想要获得藏宝图中隐藏着的秘密宝藏的地址,就需要解开藏宝图中所隐藏的秘密。
藏宝图上标记着一个
n
×
n
n \times n
n×n 大小的表格,表格中填满了各式各样的小写字母,在探索解密的过程中,kkkw可以释放法术,随意修改表格中任
意最多个格子中的字母,将其变为其他任意一个小写字母。当然,kkkw的法术也是有限制的,他最多可以修改
k
k
k 个格子上的字母。
想要得到藏宝图中的秘密,kkkw需要首先从表格的左上角开始移动,最终移动到表格的右下角为止,每一步可以往右或往下移动一格。这样,从表格的左上角移动到右下角,就一共会经过 2 n − 1 2n-1 2n−1 个格子,而这 2 n − 1 2n-1 2n−1 个格子中的小写字母将组成一段神奇的文字。而最终秘密宝藏的隐藏地点,就是最多修改个字母后,字典序最小的这一段神奇文字所描述的地方。
现在,请你帮助kkkw找到秘密宝藏被隐藏的地点。
思路
这题有点难度(不止一点)
-
采用贪心策略结合动态规划的思想:逐位确定最优字符,从第一个位置到第 (2n-1) 个位置依次选择最小可能的字母
-
用动态规划跟踪所有可能到达当前位置的坐标及剩余修改次数
-
对于每个位置,优先选择最小的字符,同时保留足够的修改次数供后续使用
代码
#include<bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
vector<string> grid(n);
for (int i = 0; i < n; ++i) {
cin >> grid[i];
}
// 存储当前可能的位置及最大剩余修改次数
unordered_map<int, int> current; // key: i * n + j, value: remaining k
int start_i = 0, start_j = 0;
int initial_k = k;
char first_char = grid[start_i][start_j];
// 处理起始位置
if (first_char != 'a' && k > 0) {
initial_k--;
current[start_i * n + start_j] = initial_k;
}
else {
current[start_i * n + start_j] = initial_k;
}
string result;
result += (initial_k < k) ? 'a' : first_char;
// 遍历每一步
for (int step = 1; step < 2 * n - 1; ++step) {
unordered_map<int, int> next_pos;
char min_char = 'z';
// 第一次遍历:找到当前步骤可能的最小字符
for (auto &entry : current) {
int key = entry.first;
int rk = entry.second;
int i = key / n;
int j = key % n;
// 尝试向右移动
if (j + 1 < n) {
int ni = i, nj = j + 1;
char c = grid[ni][nj];
char possible_char;
int new_rk;
if (rk > 0 && c > 'a') {
possible_char = 'a';
new_rk = rk - 1;
}
else {
possible_char = c;
new_rk = rk;
}
if (possible_char < min_char) {
min_char = possible_char;
}
}
// 尝试向下移动
if (i + 1 < n) {
int ni = i + 1, nj = j;
char c = grid[ni][nj];
char possible_char;
int new_rk;
if (rk > 0 && c > 'a') {
possible_char = 'a';
new_rk = rk - 1;
}
else {
possible_char = c;
new_rk = rk;
}
if (possible_char < min_char) {
min_char = possible_char;
}
}
}
// 第二次遍历:收集所有能得到最小字符的位置
for (auto &entry : current) {
int key = entry.first;
int rk = entry.second;
int i = key / n;
int j = key % n;
// 尝试向右移动
if (j + 1 < n) {
int ni = i, nj = j + 1;
char c = grid[ni][nj];
int new_rk;
if (rk > 0 && c > 'a' && 'a' == min_char) {
new_rk = rk - 1;
int new_key = ni * n + nj;
if (next_pos.find(new_key) == next_pos.end() || new_rk > next_pos[new_key]) {
next_pos[new_key] = new_rk;
}
} else if (c == min_char) {
new_rk = rk;
int new_key = ni * n + nj;
if (next_pos.find(new_key) == next_pos.end() || new_rk > next_pos[new_key]) {
next_pos[new_key] = new_rk;
}
}
}
// 尝试向下移动
if (i + 1 < n) {
int ni = i + 1, nj = j;
char c = grid[ni][nj];
int new_rk;
if (rk > 0 && c > 'a' && 'a' == min_char) {
new_rk = rk - 1;
int new_key = ni * n + nj;
if (next_pos.find(new_key) == next_pos.end() || new_rk > next_pos[new_key]) {
next_pos[new_key] = new_rk;
}
}
else if (c == min_char) {
new_rk = rk;
int new_key = ni * n + nj;
if (next_pos.find(new_key) == next_pos.end() || new_rk > next_pos[new_key]) {
next_pos[new_key] = new_rk;
}
}
}
}
result += min_char;
current = next_pos;
}
cout << result;
return 0;
}