The 18th Zhejiang Provincial Collegiate Programming Contest
写在前面
比赛地址:https://codeforces.com/gym/103055。
以下按个人难度向排序。
唐唐唐唐唐,又是死在数学手上的一次。
妈的为什么上个大学这么累好相似
A
签到。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
LL s1 = 0, s2 = 0;
for (int i = 1; i <= 5; ++ i) {
int a; std::cin >> a;
s1 += a;
}
for (int i = 1; i <= 5; ++ i) {
int a; std::cin >> a;
s2 += a;
}
std::cout << (s1 >= s2 ? "Blue" : "Red");
return 0;
}
M
签到,期望。
发现 Grammy 坑骗每个学生的过程是独立的,有答案可知 \(n=1\) 时答案为 0,由期望的性质可知 \(n\) 为任意值时答案即为 \(n\times 0 = 0\)。
Coded by hjxddl:
//Coded by hjxddl
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<iomanip>
#include<cstring>
#include<map>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N=2e5+5;
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n;
cin>>n;
cout<<fixed<<setprecision(4)<<0.0000;
}
L
签到。
发现只要字符串 \(T\) 中满足 \(\exists 2\le i\le |T|, T_i = T_1\) 时,即可构造:
把代码卡掉。
赛时先写了个 KMP 太唐乐。
//
/*
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
char s[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
int lth2 = read(), flag = 0;
scanf("%s", s + 1);
for (int i = 2; i <= lth2; ++ i) {
if (s[1] == s[i]) flag = 1;
}
if (!flag) std::cout << "Correct\n";
else std::cout << "Wrong Answer\n";
return 0;
}
C
模拟。
两两枚举八个点求距离,检查其中最短边长是否有 12 条;再随便枚举一个点,检查以它为顶点的三个角是否均为直角即可。
//Coded by hjxddl
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<iomanip>
#include<cstring>
#include<cmath>
#include<map>
#include<vector>
#include<queue>
#define ll long long
#define db double
using namespace std;
const db eps=1e-7;
double x[9],y[9],z[9];
int ans[5],tot;
db ans1[5],ans2[5],ans3[5];
double cul_dis(int i,int j){
return (x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])+(z[i]-z[j])*(z[i]-z[j]);
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t;
cin>>t;
while(t--){
bool vis=0;
map<pair<pair<db,db>,db>,int>mp;
for(int i=1;i<=8;i++){
cin>>x[i]>>y[i]>>z[i];
if(mp.count({{x[i],y[i]},z[i]}))vis=1;
mp[{{x[i],y[i]},z[i]}]=1;
}
if(vis){
cout<<"NO"<<"\n";
continue;
}
db dis=1e7+7;
for(int i=1;i<=8;i++){
for(int j=i+1;j<=8;j++){
dis=min(cul_dis(i,j),dis);
}
}
int cnt=0,tot=0;
for(int i=1;i<=8;i++){
for(int j=i+1;j<=8;j++){
// cout<<i<<" "<<j<<" "<<cul_dis(i,j)<<"\n";
if(fabs(cul_dis(i,j)-dis)<=eps){
if(i==1)ans[++tot]=j;
cnt++;
}
}
}
if(cnt!=12){
cout<<"NO"<<"\n";
continue;
}
for(int i=1;i<=tot;i++)
ans1[i]=x[1]-x[ans[i]],ans2[i]=y[1]-y[ans[i]],ans3[i]=z[1]-z[ans[i]];
for(int i=1;i<=tot;i++){
for(int j=1;j<=tot;j++){
if(i==j)continue;
if(fabs(ans1[i]*ans1[j]+ans2[i]*ans2[j]+ans3[i]*ans3[j])>eps)
vis=1;
}
}
if(!vis)cout<<"YES"<<"\n";
else cout<<"NO"<<'\n';
}
}
I
手玩。
现场拿打草纸撕出三条来手搓。这不是我们圣三一综合学园的标志吗,下次记得标明出处!
判断两个环是否链接在一起仅需检查两个环的交点是否不同在一侧即可。于是大力讨论:
- 若有两个环链接,一个环独立,则答案为 6。
- 若有两个环链接在同一个环上,则答案为 5。
- 若三个环两两链接,答案为 4。
若没有环链接,考虑求得三个环叠起来的顺序:
- 若三个环有序地叠在一起(如 3 在 2 上,2 在 1 上,3 在 1 上),则答案为 8。
- 若三个环之间环形地叠在一起(如 3 在 2 上,2 在 1 上,1 在 3 上),发现此时也是可以形成无法解开的稳定结构的,则答案为 7。
判断上述两种没有环链接的情况,可以考虑求得每个环下面有多少环。若每个环下面均有 1 个环则为第二种情况,否则为第一种情况。
场上没想到最后一种情况一直挂,拿三根纸条穿来穿去突然就把最后一种情况造出来了我真是 hack 数据的天才。然而扔给 dztle 实现写了个集贸呃呃战犯、、、
Code by dztle:
#include<bits/stdc++.h>
using namespace std;
#define int long long
char c[10][10];
bool ok[10];
int cnt=0;
int rk[4];
signed main(){
for(int i=1;i<=6;++i){
scanf("%s",c[i]);
if(c[i][0]=='t') ok[i]=1;
else ok[i]=0;
}
if(ok[1]^ok[4]) ++cnt; //1 2
else if(ok[1]) rk[2]++;
else rk[1]++;
if(ok[2]^ok[5]) ++cnt; //1 3
else if(ok[2]) rk[3]++;
else rk[1]++;
if(ok[3]^ok[6]) ++cnt; //2 3
else if(ok[3]) rk[3]++;
else rk[2]++;
if(cnt==0){
bool fl=0;
for(int i=1;i<=3;++i){
if(rk[i]==2){
fl=1;
}
}
if(fl) cout<<8;
else cout<<7;
}
if(cnt==1) cout<<6;
if(cnt==2) cout<<5;
if(cnt==3) cout<<4;
return 0;
}
J
BFS。
队友写的看都没看。
Code by dzlte:
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){
int x=0,f=1; char s;
while((s=getchar())<'0'||s>'9'){
if(s=='-') f=-1;
}
while(s>='0'&&s<='9'){
x=x*10+(s^'0');
s=getchar();
}
return x*f;
}
const int N=3005;
int n,m,T,a[N],head[N],tot;
struct node{
int to,nxt;
}e[N<<1];
inline void add(int u,int v){
e[++tot].nxt=head[u],head[u]=tot,e[tot].to=v;
}
int w[N],f[N],q[N],top,bo;
void bfs(){
bo=1;
q[++top]=1;
memset(w,0x3f,sizeof(w));
w[1]=0;
while(top>=bo){
int u=q[bo];
++bo;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(w[v]>w[u]+1){
w[v]=w[u]+1;
q[++top]=v;
}
}
}
}
signed main(){
n=read(),m=read(),T=read();
for(int i=2;i<=n;++i){
a[i]=read();
}
for(int i=1,u,v;i<=m;++i){
u=read(),v=read();
add(u,v);
add(v,u);
}
bfs();
f[0]=0;
for(int i=2;i<=n;++i){
for(int j=w[i]*2;j<=T;++j){
f[j]=max(f[j],f[j-w[i]*2]+a[i]);
}
}
for(int i=1;i<=T;++i){
cout<<f[i]<<' ';
}
return 0;
}
F
数学,整除分块。
妈的都忘了这东西了我是数论麻瓜。
记最后剩余 \(n'\) 个人,则此时最少应将每个人手中的物品调整为 \(\left\lceil \frac{m}{n'} \right\rceil\) 个。则最少操作次数为:
有 \(1\le n'\le n\),由整除分块可知,\(\left\lceil \frac{m}{n'} \right\rceil\) 仅有 \(\sqrt{n}\) 种取值,对于每一种取值为了最小化上式应取满足该取值的最小的 \(n'\)。于是考虑整除分块枚举 \(\left\lceil \frac{m}{n'} \right\rceil\) 对应的每一种取值的 \(n'\) 的区间检查上式求最小值即可。
总时间复杂度 \(O(T\sqrt{n})\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
int n, m; std::cin >> n >> m;
LL ans = 1e9;
//(n - nn) + ceil(m / nn) * nn - m
//(ceil(m / nn) - 1) * nn + n - m
for (LL j = n, i; j; j = i - 1) {
i = ceil(1.0 * m / ceil(1.0 * m / j));
LL k = ceil(1.0 * m / j) - 1;
ans = std::min(ans, k * i + n - m);
}
std::cout << ans << "\n";
}
return 0;
}
G
并查集。
一个显然的想法是先使用 map
建立位置与编号的映射,然后使用并查集维护所有连通块的边界数量。
考虑加入一个位置对所在连通块的影响。发现仅需考虑六个相邻位置,且每有一个已存在的相邻位置均会令所在连通块的边界数量减 2,并查集合并时直接维护即可。
总时间复杂度 \(O(q\log q\alpha(q))\)
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 2e6 + 10;
const int ex[6] = {1, 0, -1, -1, 0, 1};
const int ey[6] = {0, 1, 1, 0, -1, -1};
//=============================================================
int nodenum, fa[kN], sz[kN];
std::map <pii, int> id;
//=============================================================
int getid(int x_, int y_) {
if (!id.count(mp(x_, y_))) return -1;
return id[mp(x_, y_)];
}
int find(int x_) {
return fa[x_] == x_ ? x_ : fa[x_] = find(fa[x_]);
}
void merge(int x_, int y_) {
int fax = find(x_), fay = find(y_);
if (fax == fay) return ;
fa[fax] = fay;
sz[fay] += sz[fax];
}
void newnode(int x_, int y_) {
id[mp(x_, y_)] = ++ nodenum;
fa[nodenum] = nodenum;
sz[nodenum] = 6;
int p = nodenum, cnt = 0;
for (int i = 0; i < 6; ++ i) {
int q = getid(x_ + ex[i], y_ + ey[i]);
if (q == -1) continue;
merge(p, q);
cnt += 2;
}
sz[find(p)] -= cnt;
return ;
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int q; std::cin >> q;
while (q --) {
int opt, x, y; std::cin >> opt >> x >> y;
if (opt == 1) {
newnode(x, y);
} else {
std::cout << sz[find(getid(x, y))] << "\n";
}
}
return 0;
}
/*
8
1 0 0
2 0 0
1 0 2
1 1 2
1 0 3
2 0 3
1 0 1
2 0 0
*/
D
二叉树,最短路。
本场好玩题。
发现对于所有询问 \(s\rightarrow t\) 的路径的形态,一定是先从 \(s\) 到达 \(s, t\) 的某个公共前缀,然后再从公共前缀到 \(t\)。由于为无向图,且公共前缀个数仅有 \(\log n\) 个,一个显然的想法是对于每次询问枚举 \(s, t\) 的所有公共前缀 \(u\),将 \(u\) 作为中转点,则其贡献即为 \(\operatorname{distance}(u, s) + \operatorname{distance}(u, t)\),所有公共前缀的贡献最小值即为答案。\(\operatorname{distance}(u, v)\) 代表点 \(u\) 到点 \(v\) 的最短路,即需要以所有数 \(u\) 为起点,在以 \(u\) 为前缀的所有点构成的子图中跑单源最短路。
考虑对于所有点按照前缀关系建立一棵树。对于点 \(u\) 记其父节点为 \(\left \lfloor \frac{u}{2}\right\rfloor\)(相当于右移,即删去二进制最后一位),则以点 \(u\) 为根子树中所有点即为以 \(u\) 为前缀的所有点。发现这个建树过程即为线段树的建树过程。这是一棵以 1 为根的二叉树,且树高仅为 \(O(\log n)\) 级别。由树的形态可知,以 \(u\) 为前缀的所有点仅有 \(O(2^{\log n - \log u})\approx O\left(\frac{n}{u}\right)\) 个,每条边仅会在 \(O(\log n)\) 个子图中出现。
于是可以考虑构建这棵树后在树上 DFS 维护子树内点集,然后直接在此点集中以根节点为起点跑 Dijkstra 来预处理 \(\operatorname{distance}\)。从根节点向下每次分叉都会使点集大小减半,则由递归分析法可知,均摊的预处理总时间复杂度为 \(O((n + m)\log^2 (n + m))\) 级别。
预处理后每次询问仅需在这棵树上暴跳求得 \(s, t\) 的所有公共祖先 \(u\),求以它们为中转点的贡献 \(\operatorname{distance}(u, s) + \operatorname{distance}(u, t)\) 并取最小值即为询问答案,考虑使用 map
存 \(\operatorname{distance}\) 则单次询问时间复杂度 \(O(\log^2 n)\) 级别。
综上,总时间复杂度为 \(O((n + m) \log^2 (n + m) + q\log^2 n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 1e5 + 10;
const int kM = 4e5 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, m;
int edgenum, head[kN], v[kM], w[kM], ne[kM];
LL dis[kN];
bool vis[kN];
std::map<pii, LL> distance;
int tag[kN];
//=============================================================
void Add(int u_, int v_, int w_) {
v[++ edgenum] = v_;
w[edgenum] = w_;
ne[edgenum] = head[u_];
head[u_] = edgenum;
}
std::vector<int> Init(int now_) {
std::vector<int> node;
if (now_ > n) return node;
node.push_back(now_);
std::vector<int> lnode = Init(now_ << 1);
std::vector<int> rnode = Init(now_ << 1 | 1);
std::priority_queue<std::pair<LL, int> > q;
for (auto u: lnode) node.push_back(u);
for (auto u: rnode) node.push_back(u);
for (auto u: node) {
dis[u] = kInf;
vis[u] = 0;
}
dis[now_] = 0;
q.push(mp(0, now_));
while (!q.empty()) {
int u_ = q.top().second; q.pop();
if (vis[u_]) continue;
vis[u_] = 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
q.push(mp(-dis[v_], v_));
}
}
}
for (auto u: node) distance[mp(now_, u)] = dis[u];
return node;
}
LL Query(int tag_, int s_, int t_) {
LL ans = kInf;
for (int u = s_; u; u >>= 1) tag[u] = tag_;
for (int u = t_; u; u >>= 1) {
if (tag[u] == tag_) {
ans = std::min(ans, distance[mp(u, s_)] + distance[mp(u, t_)]);
}
}
return (ans >= kInf ? -1 : ans);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int u_, v_, w_; std::cin >> u_ >> v_ >> w_;
Add(u_, v_, w_), Add(v_, u_, w_);
}
Init(1);
int q; std::cin >> q;
while (q --) {
int s, t; std::cin >> s >> t;
std::cout << Query(q + 1, s, t) << "\n";
}
return 0;
}
B
扫描线,二维数点
扫描线+扫描线妈的,可能算是二维数点套路题。
记左下角为点 \((0, 0)\),右上角为点 \((1, 1)\) 的面积为 1 的单位矩形编号为 \((1, 1)\),则对于左下角为点 \((x_a, y_a)\) 右上角为点 \((x_b, y_b)\) 的矩形,其覆盖的单位矩形的编号为 \((x_a + 1, y_a + 1)\) 到 \((x_b, y_b)\)。
发现坐标的范围不大,一个想法是对于每次询问 \((s, t)\),考虑每个坐标是否会对这次询问产生贡献,即判断对于该坐标,覆盖它的给定矩形中是否存在一个矩形 \(i\) 满足 \((i<s) \lor (i > t)\)。这个套路好熟悉在牛客练习赛122里见过。考虑对于每个单位矩形 \((i, j) (1\le i,j\le 2000)\) 预处理 \(\operatorname{minid}(i, j), \operatorname{maxid}(i, j)\) 分别表示覆盖单位矩形 \((i, j)\) 的编号最小、最大的给定矩形的编号。则询问 \((s, t)\) 的答案即为:
套路地取补集,记所有被覆盖过的单位矩形数量为 \(s\),即求:
考虑扫描线预处理 \(\operatorname{minid}(i, j), \operatorname{maxid}(i, j)\),将所有给定矩形按 \(x\) 坐标排序,按照 \(x\) 坐标递增进行扫描线进行矩形的插入和删除,线段树维护当前 \(x\) 坐标的一行各单位矩形被覆盖的情况。考虑在每个线段树节点上一个大根堆一个小根堆,维护覆盖该节点代表区间的所有矩形的编号,对每行更新完毕后遍历整棵树即可求所有叶节点代表的单位矩形的 \(\operatorname{minid}(i, j), \operatorname{maxid}(i, j)\)。注意为了保证复杂度,删除矩形时需要懒惰删除。
预处理后,根据上式可知,问题实际上转化为数轴上有 \(s\) 条线段 \([\operatorname{minid}(i, j), \operatorname{maxid}(i, j)]\),对于每次询问 \((s, t)\)答案即为 \(s\) 减去被区间 \([s, t]\) 完全覆盖的线段数量。这显然也是典中典可以扫描线+树状数组维护,考虑离线所有询问并将线段和询问按右端点排序,按照递增顺序枚举线段的右端点 \(r\),每次对于满足 \(\operatorname{maxid}(i,j) = r\) 的线段,令位置 \(\operatorname{minid}(i, j)\) 加 1,则此时即可回答 \(t = r\) 的询问,被覆盖的线段数量即为 \([s, t]\) 的区间和。
总时间复杂度 \(O\left((n + C)\log C + (C^2 + q)\log n\right)\),其中 \(C=2000\) 为坐标的范围。
妈的有点难写调了半天发现变量名写错了实在苦呀西
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const int kC = 2010;
const int limc = 2000;
//=============================================================
int n, q, miny = 1, maxy = limc;
struct Rectangle {
int id, xa, ya, xb, yb;
} a[kN], b[kN];
bool tag[kN];
int linenum;
struct Line {
int l, r;
} line[kC * kC];
struct Q {
int id, l, r;
} query[kN];
int ans[kN];
//=============================================================
bool cmp_xa(const Rectangle& fir_, const Rectangle& sec_) {
return fir_.xa < sec_.xa;
}
bool cmp_xb(const Rectangle& fir_, const Rectangle& sec_) {
return fir_.xb < sec_.xb;
}
bool cmp_line(const Line& fir_, const Line& sec_) {
return fir_.r < sec_.r;
}
bool cmp_q(const Q& fir_, const Q& sec_) {
return fir_.r < sec_.r;
}
namespace Seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
const int kNode = kC << 2;
std::priority_queue<int> minid[kNode], maxid[kNode];
void Modify(int now_, int L_, int R_, int l_, int r_, int id_) {
if (l_ <= L_ && R_ <= r_) {
minid[now_].push(-id_);
maxid[now_].push(id_);
return ;
}
if (l_ <= mid) Modify(ls, L_, mid, l_, r_, id_);
if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, id_);
return ;
}
void Query(int now_, int L_, int R_, int minid_, int maxid_) {
while (!minid[now_].empty() && tag[-minid[now_].top()]) minid[now_].pop();
while (!maxid[now_].empty() && tag[maxid[now_].top()]) maxid[now_].pop();
if (!minid[now_].empty()) {
minid_ = std::min(minid_, -minid[now_].top());
maxid_ = std::max(maxid_, maxid[now_].top());
}
if (L_ == R_) {
if (minid_ != kN && maxid_ != -kN) {
line[++ linenum] = (Line) {minid_, maxid_};
}
return ;
}
Query(ls, L_, mid, minid_, maxid_);
Query(rs, mid + 1, R_, minid_, maxid_);
}
#undef ls
#undef rs
#undef mid
}
namespace Bit {
#define lowbit(x) ((x)&(-x))
const int kLim = kN;
int lim, sum[kLim];
void Init(int lim_) {
lim = lim_;
}
void Insert(int pos_, int val_) {
for (int i = pos_; i <= lim; i += lowbit(i)) {
sum[i] += val_;
}
}
int Sum(int pos_) {
int ret = 0;
for (int i = pos_; i; i -= lowbit(i)) {
ret += sum[i];
}
return ret;
}
int Query(int L_, int R_) {
return Sum(R_) - Sum(L_ - 1);
}
#undef lowbit
}
void Init() {
std::cin >> n >> q;
for (int i = 1; i <= n; ++ i) {
int xa, ya, xb, yb; std::cin >> xa >> ya >> xb >> yb;
++ xa, ++ ya;
b[i] = a[i] = (Rectangle) {i, xa, ya, xb, yb};
}
std::sort(a + 1, a + n + 1, cmp_xa);
std::sort(b + 1, b + n + 1, cmp_xb);
for (int i = 1, j = 1, k = 1; i <= limc; ++ i) {
while (j <= n && a[j].xa == i) {
Seg::Modify(1, miny, maxy, a[j].ya, a[j].yb, a[j].id);
++ j;
}
Seg::Query(1, miny, maxy, kN, -kN);
while (k <= n && b[k].xb == i) {
tag[b[k].id] = true;
++ k;
}
}
for (int i = 1; i <= q; ++ i) {
int l, r; std::cin >> l >> r;
query[i] = (Q) {i, l, r};
}
std::sort(line + 1, line + linenum + 1, cmp_line);
std::sort(query + 1, query + q + 1, cmp_q);
Bit::Init(n);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
Init();
for (int i = 1, j = 1, k = 1; i <= n; ++ i) {
while (j <= linenum && line[j].r == i) {
Bit::Insert(line[j].l, 1);
++ j;
}
while (k <= q && query[k].r == i) {
ans[query[k].id] = linenum - Bit::Query(query[k].l, query[k].r);
++ k;
}
}
for (int i = 1; i <= q; ++ i) std::cout << ans[i] << "\n";
return 0;
}
/*
3 3
0 0 1 1
0 0 2 2
0 0 1 1
1 1
2 2
3 3
4
1
4
*/
写在最后
学到了什么:
- F:整除分块求最小值。
- I:偏序关系 or 成环。
- D:路径形态特殊,仅需考虑如何构造满足条件的路径。
- B:典中典之扫描线离线二维数点。