2020 CCPC威海训练赛复盘
队员:孔FC,王DH,李K
2020 China Collegiate Programming Contest, Weihai Site
A | B | C | D | E | F | G | H | I | J | K | L |
---|---|---|---|---|---|---|---|---|---|---|---|
S | F | A (已补) | S | F | F | A | S | F | F | F | S |
S - solved, F - failed, A - attempted
训练复盘
问题处在三道简单题A,D,H三线卡题,导致在1小时时才有提交。之后又在C题这里卡死(一个奇怪的精度问题)最后做G题的时间不太够,只做出4道题(7题为银,6题为铜)
A. Golden Spirit
题意: 2 n 2n 2n 个老人想要过桥,老人平均分布在桥的两边。每个老人都希望到桥对面再回来。老人过桥的时间均为 t t t,同时过桥后需要休息 x x x才能再过桥。老人不能单独行动,你作为一个好心人来帮老人来过桥。你过桥时间后老人相同,每次只能有一个老人过桥,问最小时间。
解:可以发现一个这样的可行解:先帮桥左边的一位老人到右边,然后帮桥右边的一位老师到左边。这样往复 2 n 2n 2n次后,原本在左边的老人全在右边,在右边的老人在左边。此时你在左边,而最开始你帮助的老人在桥右边。此时分类讨论什么时候留下先帮助左边老人,什么时候过桥帮助最开始的老人即可:
如图:
[
1
,
2
]
−
−
−
−
−
−
−
[
3
,
4
]
i
n
i
t
i
a
l
[
2
]
−
−
−
−
−
−
−
[
3
,
4
,
1
]
1
[
2
,
3
]
−
−
−
−
−
−
−
[
4
,
1
]
2
[
3
]
−
−
−
−
−
−
−
[
4
,
1
,
2
]
3
[
3
,
4
]
−
−
−
−
−
−
−
[
1
,
2
]
4
[1,2] ------- [3,4] \quad initial \\ [2] ------- [3,4,1] \quad 1\\ [2,3] ------- [4,1] \quad 2\\ [3] ------- [4,1,2] \quad 3\\ [3,4] ------- [1,2] \quad 4 \\
[1,2]−−−−−−−[3,4]initial[2]−−−−−−−[3,4,1]1[2,3]−−−−−−−[4,1]2[3]−−−−−−−[4,1,2]3[3,4]−−−−−−−[1,2]4
#include <bits/stdc++.h>
#define LL long long
using namespace std;
int main() {
// freopen("std.in","r",stdin);
// freopen("std.out","w",stdout);
int T;
scanf("%d",&T);
while (T--) {
LL n, x, t;
scanf("%lld%lld%lld",&n,&x,&t);
LL ans;
if (2*n*t >= 2*t+x)
ans = 4*n*t;
else if (2*n*t >= x + t)
ans = 4*n*t + 2*t+x-2*n*t;
else
ans = 4*n*t+t + max(x+t-2*n*t-t, (LL)0);
printf("%lld\n",ans);
}
return 0;
}
C. Rencontre
题意:给定一颗边有长度的树和三个点集,问从三个点集里各任选一个点,包含三个点的结点最少子图的边权之和为 w 求w的期望.
解:
结论一:对于树上的结点 u , v u,v u,v, u − v u - v u−v 的路径唯一
易证明结论二:对于树上的结点 u , v , w u,v,w u,v,w,包含 u , v , w u,v,w u,v,w 三个点的结点最少子图的边权之和等于 u − v , v − w , w − u u - v,v - w, w - u u−v,v−w,w−u 的路径长之和的一半。
至此,该题由三元问题转化为二元问题。
接下来考虑集合的问题:
可以发现标红的路径会和标蓝的路径在不同的三元路径集合中,因此可得集合1与集合2的每个路径的贡献值为集合3的元素个数。重复三次即可。(For arbitrary { u , v } ∈ S 1 , S 2 \{u,v\} \in {S_1, S_2} {u,v}∈S1,S2, it is contained in { u , v , w 1 } , { u , v , w 2 } , . . . , u , v , w c n t [ S 3 ] \{u,v,w_1\}, \{u,v,w_2\},...,{u,v,w_{cnt [S_3]}} {u,v,w1},{u,v,w2},...,u,v,wcnt[S3])
详细代码如下,求期望是最后需要除以三个集合的积。注意long long
转double
在前者较大时易丢失精度的问题,应该边加边除。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn = 2e5 + 7;
struct Edge {
int to, next, w;
};
Edge edges[maxn*2];
int frt[maxn], k;
void addEdge(int u, int v, int w) {
edges[++k] = (Edge){v, frt[u], w};
frt[u] = k;
}
int s[maxn];
LL edge_val[maxn];
int vis[maxn];
void dfs(int u) {
vis[u] = 1;
for (int i = frt[u]; i; i = edges[i].next) {
Edge e = edges[i];
if (vis[e.to]) continue;
edge_val[e.to] = edge_val[u] + e.w;
dfs(e.to);
s[u] += s[e.to];
}
vis[u] = 0;
}
int cnt[4];
int a[4][maxn];
LL sum[maxn];
void search(int u, LL tol, int up) {
vis[u] = 1;
sum[u] = tol;
for (int i = frt[u]; i; i = edges[i].next) {
Edge e = edges[i];
if (vis[e.to]) continue;
search(e.to, tol + (up + s[u] - s[e.to] * 2) * e.w, up+s[u]-s[e.to]);
}
vis[u] = 0;
}
LL count(int s1, int s2, int n) {
for (int i = 1; i <= n; i++)
s[i] = 0;
for (int i = 1; i <= cnt[s1]; i++)
s[a[s1][i]] = 1;
dfs(1);
// for (int i = 1; i <= n; i++)
// printf("%d ", s[i]);
// printf("\n");
LL tol = 0;
for (int i = 1; i <= cnt[s1]; i++) {
int id = a[s1][i];
tol += edge_val[id];
}
search(1, tol,0);
LL ans = 0;
for (int i = 1; i <= cnt[s2]; i++) {
int id = a[s2][i];
ans += sum[id];
//printf("%d\n",sum[id]);
}
return ans;
}
int main() {
//freopen("std.in","r",stdin);
//freopen("std.out","w",stdout);
int n;
scanf("%d",&n);
for (int i = 1; i < n; i++) {
int x, y, w;
scanf("%d%d%d",&x,&y,&w);
addEdge(x,y,w);
addEdge(y,x,w);
}
for (int i = 1; i <= 3; i++) {
scanf("%d", &cnt[i]);
for (int j = 1; j <= cnt[i]; j++)
scanf("%d", &a[i][j]);
}
double ans = 0;
ans += (double) count(1,2,n) / cnt[1] / cnt[2];
ans += (double) count(2,3,n) / cnt[2] / cnt[3];
ans += (double) count(3,1,n) / cnt[3] / cnt[1];
printf("%.12lf\n",ans/2);
return 0;
}
D. ABC Conjecture
题目大意:给一个c,设 r a d ( c ) rad(c) rad(c)表示“把c的唯一分解中出现的所有质因子都抽出一个,再乘起来”,问是否存在[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 a + b = c a+b=c a+b=c使得
r a d ( a b c ) < c rad(abc)< c rad(abc)<c
先说结论:当c的唯一分解中所有质因子的次数都是1时,是no;只要有至少一个质因子的次数不是1,就是yes。
前一个no的情况很trivial,后一个其实也蛮好证明的:
设:
c = 2 1 . . . p n . . . = 2 1 . . . p × p n − 1 . . . ( n > 1 ) c=2^1...p^n...=2^1...p\times p^{n-1}...(n>1) c=21...pn...=21...p×pn−1...(n>1)
那么将c分解为:
a = 2 1 . . . 1 × p n − 1 a = 2^1...1\times p^{n-1} a=21...1×pn−1
b = 2 1 . . . ( p − 1 ) × p n − 1 b=2^1...(p-1)\times p^{n-1} b=21...(p−1)×pn−1
显然,在最坏情况下:
n = 2 n=2 n=2
r a d ( a b c ) = 2 1 . . . ( p − 1 ) × p 1 . . < c = 2 1 . . . p × p 1 rad(abc)=2^1...(p-1)\times p^1.. < c = 2^1...p\times p^1 rad(abc)=21...(p−1)×p1..<c=21...p×p1
且易得这一分解方法对于任意 p p p 都适用。
也就是说,c是否yes取决于是否含有一个质因子,其次数大于1.
本来想的方向是大数质因子分解,后来队友提供了关键思路:
如果一个c是yes,那么必然有
c = a 2 × b ( a > 1 ) c = a^2\times b(a>1) c=a2×b(a>1)
(注意这里的ab含义与上述ab不同)
其中b的每个因子都不是完全平方数,也就是说,b的唯一分解中所有质因子的次数都是1;
既然c的规模是1e18,容易(?)想到:a和b中至少有一个是小于1e6的。
所以我们只需要算出1e6范围内的所有质数,然后边对c进行质因数分解边检查c的剩余部分是否是完全平方数;如果发现c的剩余部分非1且为完全平方数,说明剩下的部分是 a 2 a^2 a2;如果在质因数分解的过程中发现分解到的某个质因数的次数超过了1,则直接说明c是yes。
#define INL inline
#define REG register
#define U unsigned
#define M ((l+r)>>1)
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <map>
#include <memory.h>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
bool notprime[1145140];
int lst[114514],cntr;
void findprime(){
for(int i=2;i<=1000005;i++){
if(!notprime[i]){
lst[++cntr]=i;
for(int j=2;i*j<=1000005;j++){
notprime[i*j]=1;
}
}
}
}
INL bool isSq(ll n){
if(n==1){
return 0;
}
return n==((ll)sqrtl(n)*(ll)sqrtl(n));
}
int main(){
//reopen("D.in","r",stdin);
//reopen("D.out","w",stdout);
ios::sync_with_stdio(0);
findprime();
int T;
cin>>T;
while(T--){
ll n;
cin>>n;
bool ans=0;
for(int i=1,icntr=0;i<=cntr&&n>1&&!isSq(n)&&!ans;i++,icntr=0){
while(n%lst[i]==0){
n/=lst[i];
icntr++;
}
ans|=(icntr>1);
}
if(ans||isSq(n)){
cout<<"yes"<<endl;
}
else{
cout<<"no"<<endl;
}
}
return 0;
}
H - Message Bomb
题目大意:有n个聊天室以及m个人,共有一下3中操作:
1. 编号为x的人加入了y聊天室
2. 编号为x的人推出了y聊天室
3. 编号为x的人在y聊天室内发了一条消息
问最终每个人收到了几条消息
首先,以每一次的3操作为切入点肯定是不行的,是一个 O ( n m ) O(nm) O(nm)的暴力
那么,考虑从12操作切入,比较容易想到,加入一个聊天室 <=> 这个聊天室之后的消息他都能收到,退出一个聊天室 <=> 这个聊天室之后的消息他都不能收到。那么,我们只需要记录当前聊天室内收到了多少消息,加入聊天室时减去这个前缀和,退出时加上这个前缀和,就能得到这个人在聊天室内收到了多少条消息。
然而,以上思路在最后将所有人手动退出他已经加入的聊天室,因此我们需要记录每个聊天室内都有哪些人,而这个操作是一个比较恶心的操作,因此,我们考虑倒着处理输入,将前缀和变为后缀和,加入聊天室时加上这个后缀和,退出时减去这个后缀和即可。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll maxn = 2e6+5;
ll n, m;
ll t = 0;
ll num[maxn] = {0};
ll ans[maxn] = {0};
struct node{
ll x, y, k;
}a[maxn];
int main(){
cin>>n>>m>>t;
for(ll i = 1;i <= t;i++){
scanf("%lld%lld%lld", &a[i].k, &a[i].x, &a[i].y);
}
for(ll i = t;i >= 1;i--){
ll k = a[i].k;
ll x = a[i].x;
ll y = a[i].y;
//cout<<k<<" "<<x<<" "<<y<<endl;
if(k == 1){
ans[x] += num[y];
}else if(k == 2){
ans[x] -= num[y];
}else{
num[y]++;
ans[x]--;
}
}
for(ll i = 1;i <= m;i++){
printf("%lld\n", ans[i]);
}
return 0;
}
G. Caesar Cipher
题目大意:给定一个序列 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an,支持两种操作:
操作1:指定l,r,使 a i = ( a i + 1 ) m o d 65536 ( i ∈ [ l , r ] ) a_i=(a_i+1)\ mod \ 65536 (i\in [l,r]) ai=(ai+1) mod 65536(i∈[l,r]);
操作2:指定x,y,L,判断子序列 a x , a x + 1 , . . . , a x + L − 1 a_x,a_{x+1},...,a_{x+L-1} ax,ax+1,...,ax+L−1和 a y , a y + 1 , . . . , a y + L − 1 a_y,a_{y+1},...,a_{y+L-1} ay,ay+1,...,ay+L−1是否相等。
首先想不带修,只有操作2要怎么搞,显然一个哈希就搞定了;
接下来思考:如果带修,但操作1没有mod这个操作,也就是说只使 a i = a i + 1 a_i=a_i+1 ai=ai+1,要怎么做;
假设base值是p,那么对于序列 a l , a l + 1 , . . . , a r − 1 , a r a_l,a_{l+1},...,a_{r-1},a_r al,al+1,...,ar−1,ar,它的哈希值为(设 l e n = r − l + 1 len=r-l+1 len=r−l+1):
a l ⋅ p l e n − 1 + a l + 1 ⋅ p l e n − 2 + ⋯ + a r − 1 ⋅ p 1 + a r ⋅ p 0 a_l\cdot p^{len-1}+a_{l+1}\cdot p^{len-2}+\dots+a_{r-1}\cdot p^1+a_{r}\cdot p^0 al⋅plen−1+al+1⋅plen−2+⋯+ar−1⋅p1+ar⋅p0
当我们对每一位都加上1之后,哈希值就变成:
( a l + 1 ) p l e n − 1 + ( a l + 1 + 1 ) p l e n − 2 + ⋯ + ( a r − 1 + 1 ) p 1 + ( a r + 1 ) p 0 = a l ⋅ p l e n − 1 + a l + 1 ⋅ p l e n − 2 + ⋯ + a r − 1 ⋅ p 1 + a r ⋅ p 0 + ( p l e n − 1 + p l e n − 2 + ⋯ + p 1 + p ) (a_l+1) p^{len-1}+(a_{l+1}+1)p^{len-2}+\dots+(a_{r-1}+1)p^1+(a_r+1)p^0\\= a_l\cdot p^{len-1}+a_{l+1}\cdot p^{len-2}+\dots+a_{r-1}\cdot p^1+a_{r}\cdot p^0 + (p^{len-1}+p^{len-2}+\dots+p^1+p) (al+1)plen−1+(al+1+1)plen−2+⋯+(ar−1+1)p1+(ar+1)p0=al⋅plen−1+al+1⋅plen−2+⋯+ar−1⋅p1+ar⋅p0+(plen−1+plen−2+⋯+p1+p)
也就是说,相当于在这个区间的哈希值上加上了 ∑ k = 0 l e n − 1 p k \sum^{len-1}_{k=0} p^k ∑k=0len−1pk,
也就是base数组的前len-1项前缀和。
前缀和可以预处理出来,区间加可以在一颗线段树上维护,上推操作就是字符串合并时对哈希的操作。
现在考虑最后一个问题:加上mod 65536(即“溢出”)之后怎么办?
我们注意到这题的特点:每次区间加操作都只加1,最多5e5次操作
显然,对于每一个位置,溢出事件发生的次数是至多 ⌈ 5 × 1 0 5 65536 ⌉ = 8 \lceil \frac{5\times 10^5}{65536} \rceil =8 ⌈655365×105⌉=8次,那么对于每一个可能出现溢出的位置,直接在线段树上暴力递归修改,返回的时候也是暴力上推即可。由于发生次数少,时间复杂度是正确的。
(思路就是这样,代码之后再贴,现在的代码一直是WA的)
L - Clock Master
题目大意:
定义对任意自然数k和任意数组 { t n } \{t_n\} {tn},(kmodt1, kmodt2, ……, kmodtn)为一个组合
给一个数字 b b b,让你将构造一个数组 { t n } \{t_n\} {tn},满足 ∑ t n < b \sum t_n < b ∑tn<b,使得可以得到的互不相同的组合最多,输出 l n ln ln(组合个数)
实际上,组合个数就是lcm(ti), 问题转化为了使得{tn}的lcm最大。一个想法就是使得ti为p^k,其中p为质数。
那么问题就变成构造 p 1 k 1 + p 2 k 2 + p 3 k 3 + ⋯ + p n k n < b p^{k1}_1+p^{k2}_2+p^{k3}_3+\dots+p^{kn}_n < b p1k1+p2k2+p3k3+⋯+pnkn<b,使得 l n ( p 1 k 1 ⋅ p 2 k 2 ⋅ p 3 k 3 ⋅ ⋯ ⋅ p n k n ) ln(p^{k1}_1\cdot p^{k2}_2\cdot p^{k3}_3\cdot\dots\cdot p^{kn}_n ) ln(p1k1⋅p2k2⋅p3k3⋅⋯⋅pnkn) 最大
猜想暴力枚举加记忆化搜索可能可行,但是我还没有验证。
考场上的思路是dp,暴力枚举质数pi以及幂次ki,转移方程 d p [ i ] = m a x { d p [ i ] , d p [ i − p k ] + k l n ( p ) } dp[i] = max\{dp[i], dp[i-p^k]+kln(p)\} dp[i]=max{dp[i],dp[i−pk]+kln(p)},时间复杂度 O ( n 2 l o g n ) O(\frac{n^2}{log\ n}) O(log nn2)
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e5+5;
int pri[maxn] = {0};
int cnt = 0;
bool vis[maxn] = {0};
long double dp[maxn] = {0};
long double lg[maxn] = {0};
int ans = 0;
void pre(){
for(int i = 2;i <= 3e4;i++){
lg[i] = log(i);
if(!vis[i]){
pri[++cnt] = i;
}
for(int j = 1;j <= cnt &&pri[j]*i<= 3e4;j++){
vis[pri[j]*i] = true;
if(i%pri[j] == 0)break;
}
}
for(int p = 1;p <= cnt;p++){
for(int i = 30000;i >= 1;i--){
int m = pri[p];
while(m <= i){
dp[i] = max(dp[i], dp[i-m]+lg[m]);
m *= pri[p];
}
}
}
}
int main(){
pre();
int t = 0;
cin>>t;
int x;
while(t--){
scanf("%d", &x);
printf("%.7Lf\n", dp[x]);
}
return 0;
}