BalticOI 2023 题解
一切问题欢迎与我联系
Astronomer
计算几何,跳了。
Staring Contest
很简单啊,注意数值各不相同。
对于 \(n=2\) 的情况,询问一次然后直接输出即可。
对于 \(n=3\) 的情况,询问 \(1,2\) 得到 \(p\),询问 \(2,3\) 得到 \(q\),分讨一下。
- 若 \(q>p\),则 \(2\) 为 \(p\)。
- 若 \(q<p\),则 \(3\) 为 \(q\)。
- 若 \(q=p\),则 \(1\) 为 \(p\)。
情况一二是好的,仍有一个有用的信息,可以继续进行下去。若只有情况一二,则询问次数为 \(n\)。
情况三不好,此时没有有用的信息了。若只有情况三,则询问次数为 \(2n\)。
但若出现情况三则 \(a_1<a_2\) 且 \(a_1<a_3\),随机选择的话出现概率为 \(\frac{1}{3}\)。
\(a_1\) 的值越大则概率越小,此时已知 \(a_2,a_3\) 均大于 \(a_1\),下一次选择 \(a_2/a_3\) 充当 \(1\),会较优。
感觉可过,实际如此,证明是不会的。
与官解思路相同。
证明或许类似快排,期望下会多 \(O(logn)\) 次?
有没有人教我证明啊?
#include<bits/stdc++.h>
using namespace std;
const int N=1500+100;
mt19937 rnd(time(0));
#define gc getchar()
#define rd read()
inline int read(){
int x=0,f=0; char c=gc;
for(;c<'0'||c>'9';c=gc) f|=(c=='-');
for(;c>='0'&&c<='9';c=gc) x=(x<<1)+(x<<3)+(c^48);
return f?-x:x;
}
int n,id[N],a[N];
inline int ask(int i,int j){ printf("? %d %d", id[i], id[j]); cout<<endl; return rd; }
inline void is(int i,int v){ a[id[i]]=v; }
inline void over(){ printf("! "); for(int i=1;i<=n;++i) printf("%d ", a[i]); cout<<endl; exit(0); }
queue<int> Q;
int sol(int x,int y,int p){
while(!Q.empty()){
int z=Q.front(),q=ask(x,z); Q.pop();
if(p==q) return is(x,p),sol(y,z,ask(y,z));
else if(q>p) return is(y,p),sol(x,z,q);
else is(z,q);
}
is(x,p),is(y,p),over();
}
int main(){
n=rd;
for(int i=1;i<=n;++i) id[i]=i;
shuffle(id+1,id+n+1,rnd);
for(int i=3;i<=n;++i) Q.push(i);
sol(1,2,ask(1,2));
return 0;
}
Tycho
将模 \(p\) 为 \(0\) 的时间称为零点。当走到点 \(x\) 时,有两种选择:
- 等到最近的零点。
- 直接冲。
由于初始点为零点,因此每个点的最优解都可以由之前的某个点从零点出发得到。
记 \(f_i\) 为到达点 \(i\) 且时间为零点的最小花费。DP 计算出所有 \(f_i\),再计算从每个点出发直接冲终点的最小花费,即为答案。
得到了 \(O(n^2)\) 做法,考虑优化。
按 \(a_j\%p\) 分讨一下,发现可以线段树优化。
时间复杂度 \(O(nlogn)\)。
注意:最大值会超过 \(10^{18}\),\(INF\) 应初始化为 \(2*10^{18}\)。
与官解思路相同。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+100,INF=2e18;
#define gc getchar()
#define rd read()
inline int read(){
int x=0,f=0; char c=gc;
for(;c<'0'||c>'9';c=gc) f|=(c=='-');
for(;c>='0'&&c<='9';c=gc) x=(x<<1)+(x<<3)+(c^48);
return f?-x:x;
}
int b,p,d,n,a[N],f[N],y[N];
int ls[N],l_c=0;
#define lc (id<<1)
#define rc (id<<1|1)
#define mid (l+r>>1)
int mn[N<<2];
void pushup(int id){ mn[id]=min(mn[lc],mn[rc]); }
void bui(int id,int l,int r){
if(l==r) return mn[id]=INF,void();
bui(lc,l,mid),bui(rc,mid+1,r),pushup(id);
}
void chg(int id,int l,int r,int ql,int v){
if(l==r) return mn[id]=min(mn[id],v),void();
ql<=mid?chg(lc,l,mid,ql,v):chg(rc,mid+1,r,ql,v); pushup(id);
}
int ask(int id,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return mn[id];
int res=INF;
if(ql<=mid) res=min(res,ask(lc,l,mid,ql,qr));
if(qr>=mid+1) res=min(res,ask(rc,mid+1,r,ql,qr));
return res;
}
signed main(){
b=rd,p=rd,d=rd,n=rd;
for(int i=1;i<=n;++i) a[i]=rd,ls[++l_c]=y[i]=a[i]%p;
ls[++l_c]=0,sort(ls+1,ls+l_c+1),l_c=unique(ls+1,ls+l_c+1)-ls-1;
for(int i=0;i<=n;++i) y[i]=lower_bound(ls+1,ls+l_c+1,y[i])-ls;
bui(1,1,l_c),chg(1,1,l_c,y[0],0);
for(int i=1;i<=n;++i){
f[i]=a[i]/p*p+a[i]/p*d+min(y[i]-1>=1?ask(1,1,l_c,1,y[i]-1)+p:INF,ask(1,1,l_c,y[i],l_c)-d);
chg(1,1,l_c,y[i],f[i]-a[i]/p*p-a[i]/p*d);
}
int ans=INF;
for(int i=0;i<=n;++i) ans=min(ans,f[i]+b-a[i]+(b-a[i]-1)/p*d);
printf("%lld\n", ans);
return 0;
}
Minequake
猜测最优方案一定是一次 dfs 的过程。
考虑遍历儿子的顺序,小小计算一下发现与顺序无关。
这样就得到了 \(O(n^2)\) 做法。
正解显然是换根 DP。
考虑第一次 DP,记 \(f_u\) 为从 \(u\) 开始遍历 \(u\) 的整棵子树需要的最小花费。
换根 DP 即可。
时间复杂度 \(O(n)\)。
貌似与官解思路相同?(马蜂太抽象了)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int N=1e5+100,INF=1e18;
#define gc getchar()
#define rd read()
inline int read(){
int x=0,f=0; char c=gc;
for(;c<'0'||c>'9';c=gc) f|=(c=='-');
for(;c>='0'&&c<='9';c=gc) x=(x<<1)+(x<<3)+(c^48);
return f?-x:x;
}
int n,ans=INF;
vector<int> G[N];
int siz[N],f[N],g[N];
void dfs0(int u,int fu){
siz[u]=1,f[u]=0;
for(int v:G[u]){
if(v==fu) continue;
dfs0(v,u),siz[u]+=siz[v];
f[u]+=f[v]-siz[v]*siz[v];
}
f[u]+=siz[u]*(siz[u]-1);
}
void dfs1(int u,int fu){
ans=min(ans,g[u]);
for(int v:G[u]){
if(v==fu) continue;
int cpgu=g[u]-(siz[u]*(siz[u]-1)+f[v]-siz[v]*siz[v]);
int cpsizu=siz[u]-siz[v]; cpgu+=cpsizu*(cpsizu-1);
g[v]=f[v]+cpgu-cpsizu*cpsizu;
g[v]-=siz[v]*(siz[v]-1);
siz[v]+=cpsizu,g[v]+=siz[v]*(siz[v]-1);
dfs1(v,u);
}
}
signed main(){
n=rd; for(int i=1,x,y;i<n;++i) x=rd,y=rd,G[x].pb(y),G[y].pb(x);
dfs0(1,0),g[1]=f[1],dfs1(1,0);
printf("%lld\n", ans);
return 0;
}
Mineral deposits
不会。。。这种没题解的题还是少做。标程写的一坨。
正解要求我们只询问两次。
若给的 \(k*d\) 个回答是按顺序的,我们可以简单确定答案。
询问 \((-b,-b),(-b,b)\),若两个回答分别为 \(p,q\),则点为 \((\frac{p+q}{2}-2b,\frac{p-q}{2})\)。
因此我们询问 \((-b,-b),(-b,b)\) 根据回答可以得到 \(O(k^2)\) 个可能解。
考虑通过一次询问判断解是否可行。
对于点 \((x,y)\),我们找之前未出现过的的 \(dt\) ,取四个点 \((0,dt),(dt,0),...\) 之一,并将取该点可能会产生的距离标记一下。
按每个可行解的 \(dt\) 排序,每次查询是否有该值,若有则加入答案,并将其产生的距离消去。
考虑如何确定某个点是否可以作为询问点。
对于点 \(u=(x,y)\) 的确定,要取点 \(q=(x',y')\) 作为询问点。
- \(q\) 点位置不合法则不可行。
- 存在与 \((x,y)\) 不同的点 \(p\) 满足 \(p\) 的 \(dt\) 未确定且 \(dis(p,q)<=dt_u\) 则不可行。
- 存在与 \((x,y)\) 不同的点 \(p\) 满足 \(p\) 的 \(dt\) 已确定且 \(dis(p,q)<=dt_p\) 则不可行。
- 其他情况可行。
用一个类似并查集的精妙做法来循环确定可行解的 \(dt\) 即可(详见标程)。
至于为什么一定有解?可以询问的范围是 \([-1e8,1e8]\),而 \(k<=20\)。
感觉是标程的解释。。。
贴一下标程
#include <cstdlib>
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define com complex<double>
#define ld long double;
#define ii pair<ll,ll>
#define vi vector<int>
#define vll vector<ll>
#define vvi vector<vi>
#define vvll vector<vll>
#define vii vector<ii>
#define FOR(x,n) for(int x=0;x<(int)(n);x++)
#define FORS(x,n) for(int x=1;x<=(n);x++)
#define FORE(x,a) for(auto &x: a)
#define FORT(x,a,b) for(int x=(a);x<(b);x++)
#define FORD(x,a,b) for(int x=(a);x>=(b);x--)
#define ALL(x) x.begin(),x.end()
#define REP(n) for(int _ = 0; _ < n; _++)
#define MT make_tuple
#define MP make_pair
#define pb push_back
#define F first
#define S second
#define vvvll vector<vvll>
#define sz(x) ((int)x.size())
int b, k, w;
int lm = 1e8;
ll dist(ii x, ii y) {
return abs(x.F - y.F) + abs(x.S - y.S);
}
vll query(vii qs) {
cout << "? ";
FORE(q,qs) {
cout << q.F << " " << q.S << " ";
}
cout << endl;
vll res(k*qs.size());
FOR(i,(k*qs.size())) {
cin >> res[i];
}
return res;
}
set<int> dd;
vii qps;
vi ccl;
vii cpq;
int cc = 0;
vii pts;
int cp;
bool try_cc(int idx, int dx, int dy){
ii qp = {pts[idx].F + dx,pts[idx].S + dy};
if(qp.F < -lm || qp.F > lm || qp.S < -lm || qp.S > lm) return false;
FOR(j,cp) {
ll dj = dist(qp,pts[j]);
if(j == idx) continue;
if(ccl[j] < 0 && dj <= abs(dx) + abs(dy)) return false;
if(dj <= ccl[j]) return false;
}
//cout << "sb" << endl;
FOR(j,cp) {
dd.insert(dist(qp,pts[j]));
}
qps.pb(qp);
cpq[idx] = qp;
return true;
}
int main() {
cin >> b >> k >> w;
// Assumptions
vll tt;
vll a = query({{-b,-b},{-b,b}});
set<ii> init;
FORE(p,a) FORE(q,a) {
if((p+q-2*b)%2) continue;
ll x = (p + q - 2*b)/2-b;
ll y = p - (b + x) - b;
//cout << " " << x << " " << y << endl;
if(y < -b || y > b || x < -b || x > b) continue;
init.insert({x,y});
}
pts = vii(ALL(init));
cp = pts.size();
cpq.resize(cp);
ccl.resize(cp,-1);
int idx = 0;
int fd = 0;
int cc = 0;
vi nxt(cp);
iota(ALL(nxt),1);
nxt[cp-1] = 0;
int pr = cp-1;
int hic = 1;
while(fd < cp) {
if(idx <= pr){
if(!hic) cc++;
hic = 0;
}
//cout << cc << " " << idx << " " << pr << endl;
if(try_cc(idx,cc,0) || try_cc(idx,-cc,0) || try_cc(idx,0,cc) || try_cc(idx,0,-cc)) {
ccl[idx] = cc;
fd++;
nxt[pr] = nxt[idx];
} else {
pr = idx;
}
while(dd.count(cc)) {
cc++;
hic = 1;
}
idx = nxt[idx];
}
vll ds = query(qps);
map<ll,int> cnt;
FORE(d,ds) cnt[d]++;
vii pem(cp);
FOR(i,cp) {
pem[i] = {ccl[i],i};
}
sort(ALL(pem));
vii sol;
FOR(pmi,cp) {
int pi = pem[pmi].S;
ii p = pts[pi];
if(cnt[ccl[pi]]) {
sol.pb(p);
FORE(q,qps) cnt[dist(p,q)]--;
}
}
cout << "! ";
FORE(s,sol) cout << s.F << " " << s.S << " ";
cout << endl;
}
Sequence
艹,看错题了。
当确定 \(x\) 时,最优的序列一定是单增的,最大值一定为 \(x\),序列长度是 \(O(logn)\) 级别的。
感性理解序列个数不会太多,考虑将序列记为状态,进行 DP。
有两种转移:
- \(x-1 \to x\)。
- \(d,\frac{x}{d} \to x\)。
具体实现时可以用记忆化搜索,用 set 储存状态,map 记忆。
然后就 T 了。。。
将状态作为全局变量,需要时撤回即可。
手写 set。由于序列长度很小,加入元素时可以暴力加入并记录加入位置,删除可以做到 \(O(1)\) 了。
时间复杂度大概是 \(O(nlog^2n)/O(nlog^3n)\) 这样?不会分析了。
有没有人会分析时间复杂度啊?
思路与官解相同。
#include<bits/stdc++.h>
using namespace std;
using db=double;
#define pb push_back
const int N=3e4+100,MOD=998244353,INF=1e9+7;
#define gc getchar()
#define rd read()
inline int read(){
int x=0,f=0; char c=gc;
for(;c<'0'||c>'9';c=gc) f|=(c=='-');
for(;c>='0'&&c<='9';c=gc) x=(x<<1)+(x<<3)+(c^48);
return f?-x:x;
}
int n,w[N];
vector<int> s;
map<vector<int>,int> mp;
int push(int x){
int i=s.size(); for(;s[i-1]>x;--i);
return s[i-1]==x?-1:(s.insert(s.begin()+i,x),i);
}
void pop(int i){
if(i==-1) return;
s.erase(s.begin()+i);
}
int dfs(){
if(mp.count(s)) return mp[s];
if(s.size()==1) return mp[s]=w[1];
int res=INF,x=*s.rbegin(),f; s.pop_back();
f=push(x-1),res=min(res,dfs()),pop(f);
for(int d=2,f1,f2;d*d<=x;++d) if(x%d==0) f1=push(d),f2=push(x/d),res=min(res,dfs()),pop(f2),pop(f1);
push(x); return mp[s]=res+w[x];
}
void solve(){
n=rd; for(int i=1;i<=n;++i) w[i]=rd;
s={1}; for(int i=1,f;i<=n;++i) f=push(i),printf("%d\n", dfs()),pop(f);
}
int main(){
// freopen("037-hg-random-1.in","r",stdin);
// freopen("Elaina.out","w",stdout);
int T=1;
while(T--) solve();
return 0;
}