Codeforces Global Round 30 (Div. 1 + Div. 2)

Codeforces Global Round 30

A

\(n\) 的数组 \(a\),每可以选一个 \(x\) 满足 \(\operatorname{min}(a_i,a_{i+1})\le x \le \operatorname{max}(a_i,a_{i+1})\),然后去掉 \(a_i,a_{i+1}\) 并将其替换为 \(x\)。给定数 \(k\),问能否若干次操作让 \(a\) 数组只剩数字 \(k\)

———————————————————————————————————————

我们发现这个 \(=\) 是很好的性质。为了让 \(k\) 的空间尽可能大,又想到保留最大最小值。于是可以这样构造:找到最大值,把它周围的全部和他操作,\(x\) 就钦定为这个最大值。最小值也同理,把附近所有都换掉。最后只剩最大最小值,当 \(k\) 在此范围内时就输出 YES,否则 NO。

点击查看代码
#include<bits/stdc++.h> 
using namespace std; 
#define ll long long 
#define For(i,l,r) for(int i=l;i<=r;i++) 
int n;
const int N=110;
ll a[N],x,mn,mx;
void solve(){ 
    mn=1e18;mx=-1e18;
    cin>>n;
    For(i,1,n){cin>>a[i];mn=min(mn,a[i]);mx=max(mx,a[i]);}
    cin>>x;
    if(mn<=x&&x<=mx){
        cout<<"YES\n";
    }
    else cout<<"NO\n";
    
} 
int main(){ 
    ios::sync_with_stdio(0); 
    cin.tie(0);cout.tie(0); 
    int T; 
    cin>>T; 
    while(T--)solve(); 
    return 0; 
} 

B

给定 \(a_1<a_2<\cdots<a_{n-1}<a_n\),选一对 \(a_i<a_j\) 满足 \(a_j \ mod \ a_i\) 为偶数。\(n\le 10^5\)

———————————————————————————————————————

分讨,条件转化为 \(a_j-k\times a_i\) 为偶数,\(k\) 为商。如果存在两个偶数的话,显然成立。如果只有一个偶数,可以拿这个偶数试所有的奇数,可以就输出。最后只留下奇数匹配奇数的问题。

排序,现在有奇数序列 \(b_1,b_2,\cdots,b_m\)。注意到:如果 \(b_2<2\times b_1\)\(b_2\ mod\ b_1=b_2-b_1=偶数\)。也就是假如存在一段 \(b\) 序列满足相邻两数都不会凑出正确答案,那么是指数级增长的,项数最多 \(\log V\)

因此暴力枚举一对数,实际复杂度是 \(O(n\log V)\) 的。

点击查看代码
#include<bits/stdc++.h> 
using namespace std; 
#define ll long long 
#define For(i,l,r) for(int i=l;i<=r;i++) 
int n;
const int N=1e5+10;
ll a[N];
ll even[N],c0;
ll odd[N],c1;
void solve(){ 
    cin>>n;c0=c1=0;
    For(i,1,n){
        cin>>a[i];
        if(a[i]%2==0)even[++c0]=a[i];
        else odd[++c1]=a[i];
    }
    if(c0>=2){
        cout<<min(even[1],even[2])<<" "<<max(even[1],even[2])<<"\n";
        return;
    }
    if(c0==1){
        For(i,1,c1){
            ll mx=max(odd[i],even[1]);
            ll mn=min(odd[i],even[1]);
            if((mx%mn)%2==0){cout<<mn<<" "<<mx<<"\n";return;}
        }
    }
    sort(odd+1,odd+c1+1);
    For(i,1,c1){
        For(j,i+1,c1){
            if((odd[j]%odd[i])%2==0){
                cout<<odd[i]<<" "<<odd[j]<<"\n";
                return;
            }
        }
    }
    cout<<"-1\n";
} 
int main(){ 
    ios::sync_with_stdio(0); 
    cin.tie(0);cout.tie(0); 
    int T; 
    cin>>T; 
    while(T--)solve(); 
    return 0; 
} 

C

你现在身处地牢,面对着 \(m\) 个怪物和 \(n\) 把剑。

\(i\) 把剑的伤害是 \(a_i\)\(i\) 只怪物的生命值是 \(b_i\) 。用伤害为 \(x\) 的剑杀死 \(i\) 的怪物后,这把剑就会消失。然后,如果 \(c_i>0\) ,你将获得一把伤害为 \(\operatorname{max}(x,c_i)\) 的新剑。否则,你将一无所获。

现在你想知道最多可以杀死多少只怪物。注意,每只怪物你最多只能杀死一次。\(n,m\le 2\times 10^5,a,b,c\le 10^9\)

———————————————————————————————————————

剑从小往大。怪物按生命值从小往大,然后按奖励值从小往大。扫一遍,对于每把剑把能杀的怪物的奖励值存到堆里,奖励多的排前面。

因为奖励值会 \(\geq x\),所以肯定杀的完。每次杀一个就 \(+1\)。剩下弹出的操作留给后面去考虑。

点击查看代码
#include<bits/stdc++.h> 
using namespace std; 
#define ll long long 
#define For(i,l,r) for(int i=l;i<=r;i++) 
int n,m;
#define pii pair<int,int>
#define fi first
#define se second
void solve(){ 
    cin>>n>>m;
    priority_queue<int,vector<int>,greater<int> >q1;
    For(i,1,n){int a;cin>>a;q1.push(a);}
    vector<pii>M(m);
    For(i,0,m-1)cin>>M[i].fi;//b
    For(i,0,m-1)cin>>M[i].se;//c
    sort(M.begin(),M.end());
    int j=0,ans=0;
    priority_queue<int>q2;
    while(!q1.empty()){//sword
        int x=q1.top();q1.pop();
        while(j<m&&M[j].fi<=x){
            q2.push(M[j].se);
            //monsters can be killed
            j++;
        }
        if(!q2.empty()){
            int t=q2.top();q2.pop();
            ans++;if(t>0)q1.push(max(t,x));
        }
    }
    cout<<ans<<"\n";
} 
int main(){ 
    ios::sync_with_stdio(0); 
    cin.tie(0);cout.tie(0); 
    int T;cin>>T; 
    while(T--)solve(); 
    return 0; 
} 

D

要把字符串 \(s\rightarrow t\)。可以这样操作。

  • \(s'=s\)\(s'=s_i \ or \ s_{i-1}\)\(s=s'\)

最少操作几次?如果 \(k\) 次内不能完成输出 \(-1\)。否则输出变换过程。

———————————————————————————————————————

注意到,最终 \(t_i\) 这个位置一定来源于 \(s\) 中某个位置 \(p_i\)\(p_i\) 满足:

  • \(s_{p_i}=t_i\)
  • \(p_i\le i\)

前两点显然。

  • \(p_{i-1}\le p_i\)
  • \(i-k\le p_i\)

贪心的,每个位置寻找最小 \(p_i\) 可以构建最小次数。此时还有 \(k=\max(i-p_i)\)。如果超了输出 \(-1\)。否则根据找到的 \(p\) 序列逐步构造每次操作的字符串。

点击查看代码
#include<bits/stdc++.h> 
using namespace std; 
#define ll long long 
#define For(i,l,r) for(int i=l;i<=r;i++) 
const int N=1e6+10;
int n,k,p[N];
string s,t;
bool chk(int x){
    int pre=0;
    For(i,1,n){
        int L=i-x,R=i;
        bool f=0;
        For(j,max(pre,L),R){
            if(s[j]==t[i]){
                f=1;
                pre=j;break;
            }
        }
        if(!f)return 0;
    }
    return 1;
}
void solve(){ 
    cin>>n>>k>>s>>t;s=' '+s;t=' '+t;
    if(s==t){cout<<"0\n";return;}
    int l=0,r=k;
    while(l<=r){
        int mid=(l+r)>>1;
        if(chk(mid))r=mid-1;
        else l=mid+1;
    }
    if(!chk(l)){cout<<"-1\n";return;}
    int pre=0;
    For(i,1,n){
        int L=i-l,R=i;
        For(j,max(pre,L),R){
            if(s[j]==t[i]){
                pre=j;
                p[i]=i-j;
                break;
            }
        }
    }
    cout<<l<<"\n";
    string cur=s,nxt;
    For(s,1,l){
        nxt=cur;
        For(i,1,n){
            if(p[i]>0){
                nxt[i]=cur[i-1];
                p[i]--;
            }
        }
        For(i,1,n)cout<<nxt[i];
        cout<<"\n";
        swap(nxt,cur);
    }
} 
int main(){ 
    ios::sync_with_stdio(0); 
    cin.tie(0);cout.tie(0); 
    int T; 
    cin>>T; 
    while(T--)solve(); 
    return 0; 
} 

E

\(n\) 个点 \(m\) 条边 \((u,v,w)\) 组成无向连通图。边的索引为输入顺序下标。假设位于顶点 \(x\) 。可多次进行以下操作:

  • 标记边 \((x,y)\),移动到 \(y\),代价是此边权。
  • 选一条 \(x\rightarrow z\) 的路径(不一定简单),移动到 \(z\)。代价是该路径上具有最大索引的边的权重。

现在位于点 \(1\),至少需要标记每一条边一次,然后返回顶点 \(1\) 。计算最小成本。\(n,m\le 10^6\)

———————————————————————————————————————

由于我们需要对每条边至少标记一次,因此最小成本为 \(\sum_{i=1}^m w_i\) 。事实上,如果该图具有欧拉回路(即每个顶点的阶数都是偶数),那么只需在这样的回路上遍历,就能实现最小代价。否则,我们可以想象在这个图中添加一些 "虚拟 "边,这样一条边的成本就是在它的两个端点之间传输的最小成本。不难看出,我们只需将所有奇数度的顶点配对,并在每对顶点之间添加一条 "虚拟 "边,这就是最佳方案。此外,我们只需计算添加这些 "虚拟 "边的最小成本。

考虑如何有效计算转移成本。建立一棵 G 的重构树 \(T\) ,其中的边 \(1 \ldots m\) 按此顺序添加。

假设我们想从 \(u\) 转移到 \(v\) 。如果 \(u\)\(v\) 的 LCA 是 \(o\) ,那么从 \(u\)\(v\) 的每一条可能路径上索引最大的边应该是 \(o\) ,或者是 \(o\) 的祖先之一。这是因为重构树的基本特性。

因此,对于 \(T\) 上的每个非叶顶点 \(i\) (即代表原始图中的一条边),预先计算每条边的最小权重,这些边是 \(i\)\(i\) 的祖先。现在我们有了 \(f_i \le f_{fa_i}\) ,这意味着将同一子树 \(p\) 中代价为 \(f_p\) 的两个顶点尽快配对是最佳选择。采用这种贪婪的方法就能通过这个问题。时间复杂度为 \(O(n \alpha(n))\) (因为我们需要在构建重构树时联合查找),空间复杂度为 \(O(n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN=2e6+5;
int n,m,fa[MAXN],val[MAXN],ff[MAXN];bool vis[MAXN],ok[MAXN];
vector<array<int,3>> E;
inline int find(int x){while(x^fa[x])x=fa[x]=fa[fa[x]];return x;}
void solve(){
	cin>>n>>m;
	ll ans=0;
	for(int i=1;i<=2*n;i++) fa[i]=i,ff[i]=0,vis[i]=ok[i]=false,val[i]=1e9;
	E.resize(m);
	for(auto &[u,v,w]:E) cin>>u>>v>>w,vis[u]^=1,vis[v]^=1,ans+=w;
	int k=n;
	for(auto [u,v,w]:E){
		u=find(u),v=find(v);
		if(u==v) val[u]=min(val[u],w);
		else{
			++k;
			if(vis[u]&&vis[v]) ok[k]=true;
			vis[k]=vis[u]^vis[v];
			fa[u]=fa[v]=ff[u]=ff[v]=k;
			val[k]=w;
		}
	}
	for(int i=2*n-1;i>=1;i--){
		if(ff[i]) val[i]=min(val[i],val[ff[i]]);
		if(ok[i]) ans+=val[i];
	}
	cout<<ans<<'\n';
}
int main(){
	// freopen("Otomachi_Una.in","r",stdin);
	// freopen("Otomachi_Una.out","w",stdout);
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	int T;cin>>T;
	while(T--) solve();
	return 0;
}

F1 & F2

\(n\) 个点,根为 \(1\) 的树 \(T\) 和长 \(n\) 的序列 \(a\),算长 \(n\) 且满足以下条件的排列的个数:

  • 对于所有 \(1 \le u \le n\) ,正好有 \(a_u\) 个顶点 \(v\) ,使得 \(v\)\(T\)\(p_v<p_u\)\(u\) 的祖先。

输出答案 \(mod \ 998244353\) 。输入数据的选择方式保证了至少存在一种有效的排列。\(n \le 5 \times 10^5\)

———————————————————————————————————————

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN=5e5+5,MOD=998244353;
const int B=1<<19;
ll ksm(ll a,int b){ll r=1;while(b){if(b&1)r=r*a%MOD;a=a*a%MOD,b>>=1;}return r;}
ll fac[MAXN],inf[MAXN],ans;
int a[MAXN],n,cnt[MAXN],siz[MAXN],hson[MAXN];
vector<int> son[MAXN],upd,posi;
vector<array<int,2>> vec[MAXN];
ll C(int x,int y){return x<0||x>y?0:fac[y]*inf[x]%MOD*inf[y-x]%MOD;}
struct segt{
int len[B*2+10];
void init(){
	for(int i=0;i<B;i++) len[i+B]=1;
	for(int i=B-1;i>=1;i--) len[i]=len[i<<1]+len[i<<1|1];
}
int qpos(int x){
	int u=1;
	while(u<B){
		if(len[u<<1]>=x) u=u<<1;
		else x-=len[u<<1],u=u<<1|1;
	}
	return u-B;
}
int gpos(int x){
	int ans=1;x+=B;
	while(x>1){
		if(x&1) ans+=len[x^1];
		x>>=1;
	}
	return ans;
}
void add(int x,int v){
	x+=B;len[x]+=v;
	while(x>1) x>>=1,len[x]+=v;
}
}T;
void dfs(int u){
	siz[u]=1;hson[u]=0;
	for(int v:son[u]){
		dfs(v);siz[u]+=siz[v];
		if(siz[v]>siz[hson[u]]) hson[u]=v;
	}
}
void dfs1(int u){
	vector<int> pth;
	for(int v=u;v;v=hson[v]){
		pth.push_back(v);
		for(int k:son[v]) if(k!=hson[v]) dfs1(k);
	}
	reverse(pth.begin(),pth.end());
	upd.clear();posi.clear();
	for(int v:pth){
		for(int k:son[v]) if(k!=hson[v]){
			for(auto [x,v]:vec[k]){
				int t=T.qpos(x);
				posi.push_back(t);
				(ans*=C(v,cnt[t]+v))%=MOD;
				cnt[t]+=v;
			}
		}
		int p=T.qpos(a[v]),q=T.qpos(a[v]+1);
		cnt[q]+=cnt[p]+1;cnt[p]=0;
		posi.push_back(q);
		upd.push_back(p);T.add(p,-1);
	}
	for(int i:posi) if(cnt[i]){
		vec[u].push_back({T.gpos(i),cnt[i]});
		cnt[i]=0;
	}
	for(int i:upd) T.add(i,1);
	upd.clear();posi.clear();
}
void solve(){
	cin>>n;
	upd.clear();
	for(int i=0;i<=n;i++) son[i].clear(),vec[i].clear(),cnt[i]=0;
	for(int i=2;i<=n;i++){
		int f;cin>>f;son[f].push_back(i);
	}
	for(int i=1;i<=n;i++) cin>>a[i],a[i]++;
	ans=1;
	dfs(1);dfs1(1);
	cout<<ans<<'\n';
}
int main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	fac[0]=inf[0]=1;T.init();
	for(int i=1;i<MAXN;i++) inf[i]=ksm(fac[i]=fac[i-1]*i%MOD,MOD-2);
	int T;cin>>T;
	while(T--) solve();
	return 0;
}

G

玛德琳正在和一台无意义机器玩。无点机器上有一棵大小为 \(n\) 的隐藏树,玛德琳必须通过询问机器找到树的边。

在查询中,玛德琳会给机器一个 \([1,2,\ldots,n]\) 的排列 \(p\) ,机器会返回一个序列 \(q_1, q_2, \ldots, q_n\) ,其中 \(q_i\) 是顶点 \(\{p_1,p_2,\ldots,p_i\}\) 形成的诱导子图的边数。

这里,由顶点子集 \(V' \subseteq V\) 形成的图 \(G(V,E)\) 的诱导子图是由该子集的顶点和原始图 \(G\) 中存在的子集顶点之间的所有边组成的图。

不过,这台机器的工作速度很慢,所以玛德琳只能在完成所有查询后才能一次性得到结果。这台机器的内存也不大,所以玛德琳只能查询 \(31\) 次。她不知道如何解决这个问题,所以她邀请你来帮助她。

请注意,交互器是非适应性。也就是说,树是事先固定好的,不会随着你的询问而改变。

———————————————————————————————————————

通过计算查询结果的差值,我们可以得到每个节点与序列中出现在它之前的节点相连的边的数量。

我们发现,通过查询节点的排列组合,然后再查询其反向排列组合,就可以得到树中每个节点的度数。一旦知道了度数,我们就可以考虑进行拓扑排序。接下来的问题就是如何找到叶节点的父节点。

将每个节点的 ID 转换为二进制。对于每个位位置 \(i\) ,将节点 \(1,2,\dots,n\) 划分为 \(a_1,a_2,\dots,a_m\) 的节点子序列(其 \(i\) -th 位为 0)和 \(b_1,b_2,\dots,b_k\) 的节点子序列(其 \(i\) -th 位为 1)。 然后,用序列 \(a_1,a_2,\dots,a_m,b_1,b_2,\dots,b_k\)\(b_1,b_2,\dots,b_k,a_1,a_2,\dots,a_m\) 进行两次查询。对于每个节点 \(u\) ,通过计算两次查询返回的前边数量的绝对差值,我们可以找到与 \(u\) 相邻的具有不同 \(i\) -th 位的节点数量。让我们用 \(f_{u,i}\) 表示这个数量。当我们在拓扑排序过程中移除叶子节点 \(x\) 时,我们可以使用 \(f_{x,i}\) 的值来确定其父节点 ID 的 \(i\) -th 位的值,从而确定父节点 \(y\) 。然后我们更新 \(y\) 的度和 \(f_{y,i}\) 的值。

由于获取度只需要一个序列及其反向序列,因此我们只需要在位分割查询之外再进行一次查询。操作总数为 \(2\lceil\log_2n\rceil + 1\) ,当 \(n = 5\times 10^4\) 时为 \(33\)

考虑将二进制分区方法扩展为三进制分区方法。对于每个三位数位置 \(i\) ,根据节点的 \(i\) 三位数是 0、1 还是 2,将节点划分为三个子序列,即 \(a_1,a_2,\dots,a_{len_1}\)\(b_1,b_2,\dots,b_{len_2}\)\(c_1,c_2,\dots,c_{len_3}\) 。然后,使用连接序列 \((a,b,c)\)\((b,c,a)\)\((c,a,b)\) 执行三次查询。对于任何给定的节点,我们都能找到其 \(i\) /th-trit 分别为 0、1 和 2 的邻居数量。其余步骤与上述相同。操作次数变为 \(3\lceil\log_3n\rceil + 1\) ,即 \(n = 5\times 10^4\)\(31\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

void Solve() {
  int n;
  cin >> n;

  int rad = 0, mmul = 1;
  while (mmul < n) {
    mmul *= 3;
    ++rad;
  }

  vector<i64> pmul(rad);
  pmul[0] = 1;

  for (int i = 1; i < rad; ++i) {
    pmul[i] = pmul[i - 1] * 3;
  }

  vector<vector<int>> pv;
  pv.reserve(rad * 3);

  for (int i = 0; i < rad; ++i) {
    array<vector<int>, 3> g;
    for (int j = 1; j <= n; ++j) {
      g[((j - 1) / pmul[i]) % 3].push_back(j);
    }

    for (int j = 0; j < 3; ++j) {
      vector<int> o;
      o.reserve(n);

      for (int k = 0; k < 3; ++k) {
        auto& w = g[(j + k) % 3];
        o.insert(o.end(), w.begin(), w.end());
      }

      pv.push_back(o);
    }
  }

  auto rf = pv[0];
  reverse(rf.begin(), rf.end());
  pv.push_back(rf);

  int pvsz = pv.size();
  cout << pvsz << endl;

  for (auto& v : pv) {
    for (int i = 0; i < n; ++i) {
      cout << v[i] << " ";
    }
    cout << endl;
  }

  vector<vector<int>> iv(pvsz, vector<int>(n + 1));
  for (int i = 0; i < pvsz; ++i) {
    i64 prv = 0;
    for (int j = 0; j < n; ++j) {
      i64 x;
      cin >> x;

      if (x == -1) {
        return;
      }

      iv[i][pv[i][j]] = x - prv;
      prv = x;
    }
  }

  vector<int> deg(n + 1);
  for (int i = 1; i <= n; ++i) {
    deg[i] = iv[0][i] + iv[rad * 3][i];
  }

  vector<i64> av(n + 1), bv(n + 1);
  for (int i = 0; i < rad; ++i) {
    int i0 = i * 3, i1 = i0 + 1, i2 = i0 + 2;
    int mul = pmul[i];
    for (int j = 1; j <= n; ++j) {
      i64 c0 = iv[i0][j], c1 = iv[i1][j], c2 = iv[i2][j];
      i64 d01 = c0 - c1, d12 = c1 - c2, d20 = c2 - c0;
      i64 degj = deg[j];
      array<i64, 3> arr{0, 0, 0};
      switch (((j - 1) / mul) % 3) {
        case 0: {
          arr = {0, d12, d20};
        } break;

        case 1: {
          arr = {d01, 0, d20};
          arr[1] = degj - arr[0] - arr[2];
        } break;

        case 2: {
          arr = {d01, d12, 0};
          arr[2] = degj - arr[0] - arr[1];
        } break;
      }

      av[j] += (arr[1] + arr[2] * 2) * mul;
    }
  }

  for (int i = 1; i <= n; ++i) {
    bv[i] = av[i] + deg[i];
  }

  queue<int> q;
  for (int i = 1; i <= n; ++i) {
    if (deg[i] == 1) {
      q.push(i);
    }
  }

  int out = 0;
  while (!q.empty() && out < n - 1) {
    int x = q.front();
    q.pop();

    if (deg[x] != 1) {
      continue;
    }

    int y = bv[x];
    cout << x << " " << y << endl;

    ++out;
    --deg[x];
    --deg[y];
    bv[y] -= x;
    if (deg[y] == 1) {
      q.push(y);
    }
  }
}

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int t;
  cin >> t;

  while (t--) {
    Solve();
  }

  return 0;
}
posted @ 2025-11-07 20:41  Accept_Reality  阅读(10)  评论(0)    收藏  举报