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\),再计算从每个点出发直接冲终点的最小花费,即为答案。

\[f_i=min_{j=0}^{i}\{f[j]+a[i]-a[j]+(p-(a[i]-a[j])\%p)\%p+(a[i]-a[j]-1)/p*d)\} \]

\[ans=min_{i=0}^{n}\{f[i]+b-a[i]+(b-a[i]-1)/p*d\} \]

得到了 \(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\) 的整棵子树需要的最小花费。

\[f_u=siz_u*(siz_u-1)+\sum_{v} f_v-siz_v^2 \]

换根 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;
}
posted @ 2024-08-20 16:26  Idtwtei  阅读(198)  评论(0编辑  收藏  举报