The 4th Universal Cup. Stage 11: Grand Prix of Southeastern Europe
Preface
最近期末要补实验,同时一直在忙着写论文,导致很长一段时间没训练了
(实则不然,其实是沉迷云顶,每天下棋下的不知天地为何物了)
还有两周就是 ECF 了,最近应该会保持一周三训稍微找回点状态
A. Sum Game
一个显著的观察就是能选的数一定在 \(1\sim m-1\) 中,并且每个数至多选一次
同时不难发现 \((1,m-1),(2,m-2),\ldots\) 不能同时出现,因此真正能选的数本质只有 \(\lceil\frac{m-1}{2}\rceil\) 个
要让这些数的任意线性组合都凑不出 \(m\) 的倍数,显然可以选 \(1,2,4,\ldots\) 来构造最优解
因此 Bob 获胜的充要条件为 \(2^{n-1}\le \lceil\frac{m-1}{2}\rceil\)
#include<cstdio>
#include<iostream>
using namespace std;
long long t,n,m;
int main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld%lld",&n,&m);
if (m==1) { puts("Ana"); continue; }
bool ok=0; long long tmp=1; --n;
while (n>0&&!ok)
{
tmp*=2LL;
if (tmp>m/2LL) ok=1;
--n;
}
puts(ok?"Ana":"Bob");
}
return 0;
}
B. AND Reconstruction
首先答案一眼具有可二分性,同时我们一眼可以按位考虑,把问题转化为只有 \(0/1\) 比特的情形
考虑如何检验 \(k\) 是否合法,先根据原序列构造出此时对应的 \(\{x_i\}\),不难发现 \(\{x_i\}\) 中的 \(1\) 能确定一段长为 \(k+2\) 的原序列的值
(除了这个 \(1\) 本身能确定原序列 \(k\) 个位置是 \(1\) 以外,同时还可以确定原序列左右两侧的数的值)
因此问题转化为一个检验若干个环上的区间能否 cover 环上的每个位置,直接差分处理一下即可
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int n,b,A[N],a[N];
inline bool check(CI k)
{
if (k==1) return 1;
static int d[N];
for (RI i=0;i<=n;++i) d[i]=0;
int cnt=0,flag=0;
for (RI i=0;i<k;++i) cnt+=a[i];
if (cnt==k) flag=1;
for (RI i=1;i<n;++i)
{
cnt-=a[i-1];
cnt+=a[(i+k-1)%n];
if (cnt==k)
{
if (i+k<n) ++d[i-1],--d[i+k+1];
else ++d[i-1],++d[0],--d[(i+k)%n+1];
}
}
for (RI i=1;i<n;++i) d[i]+=d[i-1];
if (flag)
{
++d[n-1];
for (RI i=0;i<=k;++i) ++d[i];
}
for (RI i=0;i<n;++i)
if (d[i]==0) return 0;
return 1;
}
int main()
{
scanf("%d%d",&n,&b); int k=n;
for (RI i=0;i<n;++i) scanf("%d",&A[i]);
for (RI p=0;p<b;++p)
{
for (RI i=0;i<n;++i) a[i]=((A[i]>>p)&1);
int l=1,r=k;
while (l<=r)
{
int mid=l+r>>1;
if (check(mid)) k=mid,l=mid+1; else r=mid-1;
}
}
return printf("%d\n",k),0;
}
C. XOR-Excluding Sets
首先简单分析会发现这个题实际要求的是,对于每个数求和:有多少个不包含它的集合,满足该集合中所有数异或和为 \(0\)
考虑列举出所有异或和为 \(0\) 的集合,设其数量为 \(k\),我们可以通过观察发现以下结论,对于某个数:
- 若其不出现在任意一个异或和为 \(0\) 的集合中,则它对答案的贡献为 \(k\);
- 否则它一定恰好出现在 \(\frac{k}{2}\) 个异或和为 \(0\) 的集合中,因此它对答案的贡献为 \(\frac{k}{2}\);
为了统计答案,我们需要用线性基维护异或和为 \(0\) 的集合的数目,同时还要维护不出现在任意一个异或和为 \(0\) 的集合中的数的数目
满足后者条件的数的数量是 \(O(\log a_i)\) 级别的,因此可以暴力维护这么多个线性基,总复杂度 \(O(n\log^2 a_i)\)
#include<cstdio>
#include<iostream>
#include<vector>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int mod=1e9+7,inv_2=(mod+1)/2;
struct LB
{
int base[65],free;
inline LB(void)
{
free=0; for (RI i=0;i<60;++i) base[i]=0;
}
inline void insert(int x)
{
for (RI i=59;i>=0;--i)
{
if (((x>>i)&1)==0) continue;
if (!base[i])
{
base[i]=x; return;
}
x^=base[i];
}
++free;
}
inline bool query(int x)
{
for (RI i=59;i>=0;--i)
{
if (((x>>i)&1)==0) continue;
x^=base[i];
}
return x==0;
}
}all; int n; vector <pair <int,LB>> vec;
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
signed main()
{
scanf("%lld",&n);
for (RI i=1;i<=n;++i)
{
int x; scanf("%lld",&x);
for (auto &[_,lb]:vec) lb.insert(x);
if (!all.query(x)) vec.push_back({x,all});
all.insert(x);
vector <pair <int,LB>> n_vec;
for (auto [x,lb]:vec)
if (!lb.query(x)) n_vec.push_back({x,lb});
vec=n_vec;
int un_pst=(int)vec.size(),fr=all.free;
printf("%lld\n",(quick_pow(2,i)-1LL*un_pst*quick_pow(2,fr)%mod-1LL*(i-un_pst)*quick_pow(2,fr)%mod*inv_2%mod+2LL*mod)%mod);
}
return 0;
}
D. Two Options
队友写的,我题都没看
#include <bits/stdc++.h>
constexpr int $n = 1'000'006;
using llsi = long long signed int;
constexpr llsi mod = 1'000'000'007;
int n, m;
struct minivec {
int x[2];
int length;
minivec(): x{0, 0}, length(0) {}
minivec(int a, int b): x{a, b}, length(2) {
if(x[0] > x[1]) std::swap(x[0], x[1]);
};
minivec join(const minivec &b) const {
minivec res;
int p = 0, q = 0;
while(p < length && q < b.length) {
if(x[p] == b.x[q]) res.x[res.length++] = x[p], p += 1, q += 1;
else if(x[p] < b.x[q]) p += 1;
else q += 1;
}
return res;
}
};
std::optional<minivec> rst[$n];
std::vector<int> out[$n];
bool vis[$n];
std::tuple<int, int, int> dfs(int cur) {
vis[cur] = 1;
int res1 = 1, res2 = out[cur].size(), res3 = 2;
for(auto out: out[cur]) {
if(!vis[out]) {
auto [r1, r2, r3] = dfs(out);
res1 += r1, res2 += r2;
res3 = std::min(res3, r3);
} else if(out == cur) {
res3 = 1;
}
}
return {res1, res2, res3};
}
int main() {
std::ios::sync_with_stdio(false);
std::cin >> n >> m;
while(m--) {
int i, j, x;
std::cin >> i >> j >> x;
if(rst[x]) rst[x] = rst[x]->join(minivec(i, j));
else rst[x] = minivec(i, j);
}
// std::cerr << rst[1]->length << char(10);
for(int i = 1; i <= n; ++i) if(auto r = rst[i]) {
if(r->length == 0) {
std::cout << " 0\n";
return 0;
}
if(r->length == 1) {
int x = r->x[0];
out[x].emplace_back(x);
out[x].emplace_back(x);
// std::cerr << "[Self loop] " << x << char(10);
continue;
}
auto [x, y] = r->x;
out[x].emplace_back(y);
out[y].emplace_back(x);
// std::cerr << "[Edge] " << x << "-" << y << char(10);
}
llsi ans = 1, hkr = 0;
for(int i = 1; i <= n; ++i) if(!vis[i]) {
auto [vs, es, ars] = dfs(i);
// std::cerr << std::format("dfs({}) = [vs = {}, es = {}]\n", i, vs, es);
es /= 2;
if(es > vs) {
std::cout << "0\n";
return 0;
}
if(es == vs) {
ans = ans * ars % mod;
continue;
}
if(es == vs - 1) {
ans = ans * vs % mod;
hkr += 1;
continue;
}
// std::cerr << "FUCK\n";
return 1;
}
for(int i = 1; i <= hkr; ++i) ans = ans * i % mod;
std::cout << ans << char(10);
return 0;
}
E. Phone Company
签到,显然 \(n=2k+1\),要求方案的话用循环构造,每个点向其后面的 \(k\) 个点连边即可
#include<cstdio>
#include<iostream>
using namespace std;
int k;
int main()
{
scanf("%d",&k); int n=2*k+1;
printf("%d\n",n);
for (int x=0;x<n;++x)
for (int d=1;d<=k;++d)
printf("%d%c",(x+d)%n+1," \n"[d==k]);
return 0;
}
F. Language Barrier
赛时最后 5min 调完结果 TLE on 49 了,赛后发现是 multiset 常数太大了,换成可删除的 priority_queue 就过了,只能说沟槽的卡常
首先如果 \(1\) 和 \(n\) 对应的区间有交,那么直接走过去翻译就是最优的;否则我们只考虑 \(R[1]<L[n]\) 的情形,对于令一种情况取反即可变为上述情形
不难发现此时纸条上的数一定是递增变化最优,同时走到任何一个点 \(x\) 时,能翻译的话直接把纸条上的数变为 \(R[x]\) 一定不劣
因此考虑 DP,令 \(f_x\) 表示走到 \(x\) 所需的最小代价,并钦定在点 \(x\) 处进行了一次翻译,转移就是枚举上一个走到的点 \(y\):
为了处理下标,我们可以使用扫描线的思路,把每个 \(L[i]\) 看作激活点 \(i\),\(R[i]\) 看作失活点 \(i\)
将所有 \(L[i],R[i]\) 升序排序后,在加入 \(L[x]\) 的时候,所有激活的点就是可供转移的点 \(y\)
后面的式子是一个关于距离的最小值,树上距离很容易想到用点分树处理
我们每次激活/失活某个点时,将其到根的路径上的所有点插入其权值;查询的时候同理,查询一遍其到根的路径上所有点权值的最小值即可
总复杂度 \(O(n\log^2 n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#include<set>
#include<algorithm>
#include<queue>
#define int long long
#define RI int
#define CI const int&
#define fi first
#define se second
using namespace std;
const int N=200005,INF=1e18;
int n,x,y,L[N],R[N],f[N]; vector <int> v[N];
class Tree_Solver
{
private:
int anc[N][20],dep[N];
public:
inline void DFS(CI now=1,CI fa=0)
{
anc[now][0]=fa; dep[now]=dep[fa]+1;
for (RI i=0;i<19;++i) if (anc[now][i]) anc[now][i+1]=anc[anc[now][i]][i]; else break;
for (auto to:v[now]) if (to!=fa) DFS(to,now);
}
inline int getLCA(int x,int y)
{
if (dep[x]<dep[y]) swap(x,y);
for (RI i=19;i>=0;--i) if (dep[anc[x][i]]>=dep[y]) x=anc[x][i];
if (x==y) return x;
for (RI i=19;i>=0;--i) if (anc[x][i]!=anc[y][i]) x=anc[x][i],y=anc[y][i];
return anc[x][0];
}
inline int getdis(CI x,CI y)
{
return dep[x]+dep[y]-2*dep[getLCA(x,y)];
}
}T;
namespace Point_Divide_Tree
{
int rt,ots,sz[N],mx[N],fa[N]; bool vis[N];
inline void getrt(CI now=1,CI fa=0)
{
sz[now]=1; mx[now]=0; for (auto to:v[now])
if (to!=fa&&!vis[to]) getrt(to,now),sz[now]+=sz[to],mx[now]=max(mx[now],sz[to]);
if (mx[now]=max(mx[now],ots-sz[now]),mx[now]<mx[rt]) rt=now;
}
inline void solve(int now)
{
vis[now]=1; for (auto to:v[now]) if (!vis[to])
mx[rt=0]=INF,ots=sz[to],getrt(to,now),getrt(rt,0),fa[rt]=now,solve(rt);
}
inline void divide(CI n)
{
mx[rt=0]=INF; ots=n; getrt(); solve(rt);
}
}
using namespace Point_Divide_Tree;
struct Removable_Heap
{
priority_queue <int,vector <int>,greater <int>> hp,rmv;
inline void insert(CI x)
{
hp.push(x);
}
inline void remove(CI x)
{
rmv.push(x);
}
inline int top(void)
{
while (!hp.empty()&&!rmv.empty()&&hp.top()==rmv.top())
{
hp.pop(); rmv.pop();
}
return hp.top();
}
}s[N];
signed main()
{
scanf("%lld",&n);
for (RI i=1;i<n;++i)
{
scanf("%lld%lld",&x,&y);
v[x].push_back(y);
v[y].push_back(x);
}
T.DFS(); divide(n);
for (RI i=1;i<=n;++i)
scanf("%lld%lld",&L[i],&R[i]);
if (max(L[1],L[n])<=min(R[1],R[n])) return printf("%lld\n",T.getdis(1,n)),0;
if (L[1]>R[n])
{
for (RI i=1;i<=n;++i)
swap(L[i],R[i]),L[i]=-L[i],R[i]=-R[i];
}
vector <tuple <int,int,int>> Event;
for (RI i=1;i<=n;++i)
{
Event.push_back({L[i],0,i});
Event.push_back({R[i],1,i});
}
sort(Event.begin(),Event.end());
for (RI i=1;i<=n;++i) s[i].insert(INF);
for (RI i=1;i<=n;++i)
{
f[i]=INF;
if (max(L[1],L[i])<=min(R[1],R[i])) f[i]=T.getdis(1,i)+1;
}
f[1]=0;
for (auto [_,tp,id]:Event)
{
if (tp==0)
{
int res=INF;
for (int x=id;x;x=fa[x]) res=min(res,s[x].top()+T.getdis(id,x));
f[id]=min(f[id],res+1);
// printf("f[%d] = %d\n",id,f[id]);
for (int x=id;x;x=fa[x]) s[x].insert(f[id]+T.getdis(id,x));
} else
{
for (int x=id;x;x=fa[x]) s[x].remove(f[id]+T.getdis(id,x));
}
}
return printf("%lld\n",f[n]-1),0;
}
G. Intervals from Triplets
队友写的,我题都没看
#include <bits/stdc++.h>
using llsi = long long signed int;
#define fi first
#define se second
constexpr int $n = 1'000'006;
constexpr llsi NNF = -1e16;
using ti3 = std::array<int, 3>;
int n;
ti3 hkr[$n];
std::pair<ti3, ti3> ars[$n];
llsi dp[$n][3];
int main() {
std::ios::sync_with_stdio(false);
std::cin >> n;
for(int i = 1; i <= n; ++i) {
auto &[a, b, c] = hkr[i];
std::cin >> a >> b >> c;
}
std::sort(hkr + 1, hkr + n + 1, [&](auto &&lhs, auto &&rhs) {
return lhs[1] < rhs[1];
});
int p = 1, l = 1, r = 1;
ars[0] = {{-1, -1, -1}, {-1, -1, -1}};
while(l <= n) {
while(r <= n && hkr[r][1] == hkr[l][1]) r += 1;
if(r - l > 2) {
std::cout << "-1\n";
return 0;
} else
if(r - l == 2) {
ars[p++] = {hkr[l], hkr[l + 1]};
l = r;
continue;
}
int b = hkr[l][1];
ars[p++] = {hkr[l], {b, b, b}};
l = r;
}
n = p - 1;
memset(dp, 0x80, sizeof dp);
dp[0][0] = dp[0][1] = 0;
// for(int i = 0; i <= n; ++i) std::cerr << std::format("ars[{}] = {} {} {} {} {} {}\n", i, ars[i].fi[0], ars[i].fi[1], ars[i].fi[2], ars[i].se[0], ars[i].se[1], ars[i].se[2]);
for(int i = 1; i <= n; ++i) {
if(ars[i].fi[0] >= ars[i - 1].se[2]) dp[i][0] = std::max(dp[i][0], dp[i - 1][0] + ars[i].fi[1] - ars[i].fi[0] + ars[i].se[2] - ars[i].se[1]);
if(ars[i].fi[0] >= ars[i - 1].fi[2]) dp[i][0] = std::max(dp[i][0], dp[i - 1][1] + ars[i].fi[1] - ars[i].fi[0] + ars[i].se[2] - ars[i].se[1]);
if(ars[i].fi[0] >= ars[i - 1].fi[2]) dp[i][0] = std::max(dp[i][0], dp[i - 1][2] + ars[i].fi[1] - ars[i].fi[0] + ars[i].se[2] - ars[i].se[1]);
if(ars[i].se[0] >= ars[i - 1].se[2]) dp[i][1] = std::max(dp[i][1], dp[i - 1][0] + ars[i].se[1] - ars[i].se[0] + ars[i].fi[2] - ars[i].fi[1]);
if(ars[i].se[0] >= ars[i - 1].fi[2]) dp[i][1] = std::max(dp[i][1], dp[i - 1][1] + ars[i].se[1] - ars[i].se[0] + ars[i].fi[2] - ars[i].fi[1]);
if(ars[i].se[0] >= ars[i - 1].fi[2]) dp[i][1] = std::max(dp[i][1], dp[i - 1][2] + ars[i].se[1] - ars[i].se[0] + ars[i].fi[2] - ars[i].fi[1]);
if(ars[i].se[0] != ars[i].se[1]) continue;
if(ars[i].fi[0] >= ars[i - 1].se[2]) dp[i][2] = std::max(dp[i][2], dp[i - 1][0] + ars[i].fi[2] - ars[i].fi[0]);
if(ars[i].fi[0] >= ars[i - 1].fi[2]) dp[i][2] = std::max(dp[i][2], dp[i - 1][1] + ars[i].fi[2] - ars[i].fi[0]);
if(ars[i].fi[0] >= ars[i - 1].fi[2]) dp[i][2] = std::max(dp[i][2], dp[i - 1][2] + ars[i].fi[2] - ars[i].fi[0]);
// std::cerr << "dp[" << i << "] = " << dp[i][0] << char(32) << dp[i][1] << char(10);
}
llsi ans = *std::max_element(dp[n], dp[n] + 3);
if(ans < 0) std::cout << "-1\n";
else std::cout << ans << char(10);
return 0;
}
K. Connect the Points
首先如果一条线段的两个端点不都在边界上时,显然可以直接将其忽略
只考虑那些两个端点都在边界上的线段,检验是否合法是个经典结论(这个结论还是我上半年看某篇论文的时候看到的)
根据某个方向绕边界一圈,按顺序把遇到的线段端点对应的编号记录下来
一种连边方式合法,当且仅当记录下来的编号序列是类括号序列
即用栈维护该序列,遇到栈顶两个相同的元素就出栈,最后栈为空即合法
总复杂度 \(O(n)\),不知道为什么给一个 \(n\le 7\) 的数据范围
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#include<stack>
#define RI register int
#define CI const int&
using namespace std;
int n,a,b,c,d; vector <pair <int,int>> L[4];
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i)
{
scanf("%d%d%d%d",&a,&b,&c,&d);
if ((a!=0&&a!=n&&b!=0&&b!=n)||(c!=0&&c!=n&&d!=0&&d!=n)) continue;
auto addpoint=[&](CI x,CI y,CI id)
{
if (y==0) L[0].push_back({x,id}); else
if (x==n) L[1].push_back({y,id}); else
if (y==n) L[2].push_back({x,id}); else
if (x==0) L[3].push_back({y,id});
};
addpoint(a,b,i); addpoint(c,d,i);
}
sort(L[0].begin(),L[0].end());
sort(L[1].begin(),L[1].end());
sort(L[2].begin(),L[2].end(),greater <pair <int,int>>());
sort(L[3].begin(),L[3].end(),greater <pair <int,int>>());
stack <int> stk;
for (RI i=0;i<4;++i)
for (auto [_,x]:L[i])
{
// printf("x = %d\n",x);
if (!stk.empty()&&stk.top()==x) stk.pop();
else stk.push(x);
}
puts(stk.empty()?"YES":"NO");
return 0;
}
L. Neo-Nim
好像是个繁琐的博弈+分讨,赛时后面这题我全部扔给队友了,不知道后面会不会补这个题
Postscript
感觉这场题都没啥代码难度,拿来复建一下还是挺好的

浙公网安备 33010602011771号