The 4th Universal Cup. Stage 10: Grand Prix of Wrocław
Preface
又是经典外国场,基本全是思维题,套路算法题感觉挺少的,和 ECF 的风格有点差异,不过用来找找状态也是不错的
最后 9 题收场,C 题最后半小时开的感觉还有些细节没想清楚,后续也懒得补了,只能说摆烂这一块
A. Automatized Mineral Classification
队友在我中间上厕所的时候秒的,我题目都没看
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int n;
int current = 0;
int push(int ball) {
if (ball <= 0 && ball > n) {
printf("ball = %d\n", ball);
assert(ball > 0 && ball <=n);
}
std::cout << "+ " << ball << std::endl;
int new_val; std::cin >> new_val;
int res = new_val - current;
current = new_val;
return res;
}
int pop() {
std::cout << "-" << std::endl;
int new_val; std::cin >> new_val;
int res = new_val - current;
current = new_val;
return res;
}
vector<int> ans[N];
void print_answer() {
int k = 0;
for (int i=1; i<=500; ++i) if (ans[i].size() > 0) ++k;
std::cout << "! " << k << std::endl;
for (int i=1; i<=500; ++i) if (ans[i].size() > 0) {
std::cout << ans[i].size();
for(const auto x: ans[i]) std::cout << ' ' << x;
std::cout << std::endl;
}
}
int main() {
// std::ios::sync_with_stdio(false);
vector<int> xx, yy;
cin >> n;
for (int i=1; i<=n; ++i) {
int res = push(i);
if (res==1) xx.push_back(i), ans[i].push_back(i);
else yy.push_back(i);
}
for (int i=1; i<=n; ++i) pop();
int cnt = ((n - xx.size()) + 9) / 10;
for (int blk=0, y=0; blk<cnt; ++blk, y+=10) {
// printf("blk=%d\n", blk);
vector<int> vec;
for (int i=0; i<xx.size(); ++i) push(xx[i]);
for (int i=y; i<y+10 && i<yy.size(); ++i) {
push(yy[i]);
}
for (int i=0; i<xx.size(); ++i) {
int res = pop();
if (0==res) vec.push_back(xx[i]);
}
for (int i=y; i<y+10 && i<yy.size(); ++i) pop();
for (int j=y; j<y+10 && j<yy.size(); ++j) {
for (int i=0; i<vec.size(); ++i) {
push(vec[i]);
int res = push(yy[j]);
pop();
pop();
if (res==0) {
ans[vec[i]].push_back(yy[j]);
break;
}
}
}
}
print_answer();
return 0;
}
D. DNA
这题刚开始我和徐神想了 90min 给了三四种启发式做法,结果交上去没一个对的
后面祁神一眼找到关键就直接秒了,我只能说对电波很关键
注意最后答案一定是个 0/1 串,那么其中要么 0 的个数超过一半,要么 1 的个数超过一半
因此我们钦定答案全为 0 或 1,分别跑一遍让答案取最大值即可
#include<cstdio>
#include<iostream>
using namespace std;
const int N=3005;
int t,n,nxt[3][N][2]; char s[3][N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (int i=0;i<3;++i)
{
scanf("%s",s[i]+1);
nxt[i][n+1][0]=nxt[i][n+1][1]=n+1;
for (int j=n;j>=1;--j)
{
nxt[i][j][s[i][j]-'0']=j;
nxt[i][j][(s[i][j]-'0')^1]=nxt[i][j+1][(s[i][j]-'0')^1];
}
}
auto find=[&](int v)
{
int ans=0,pos[3]={1,1,1};
while (pos[0]<=n&&pos[1]<=n&&pos[2]<=n)
{
int meets=0;
for (int i=0;i<3;++i)
meets+=(nxt[i][pos[i]][v]<=n);
ans+=(meets==3);
for (int i=0;i<3;++i)
pos[i]=nxt[i][pos[i]][v]+1;
}
return ans;
};
printf("%d\n",max(find(0),find(1)));
}
return 0;
}
F. Foxes
对于在一端动态插入/删除元素的 LIS 问题,有个经典的单调栈套栈的做法用来维护不同长度的 LIS 末端元素最优值是什么
(其实本质就是把 DP+二分 求 LIS 的算法可持久化了,只能说这个还是很套路的)
因此我们在移动指针时对于左右两侧分别维护这个结构,所有的修改操作都很 trivial 了(只需要简单二分即可),然后考虑如何合并左右两侧的答案
考虑用权值线段树,对于左侧的最优取值点 \(x\),我们对 \([x+1,10^6]\) 的后缀作区间加;对于右侧的最优取值点 \(x\),我们对 \([1,x]\) 的前缀作区间加,全局的 LIS 最大值就是 \([1,10^6]\) 所有位置的最大值
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,M=1e6+1;
int n,q,a[N];
class Segment_Tree
{
private:
int mx[N<<2],tag[N<<2];
inline void pushup(CI now)
{
mx[now]=max(mx[now<<1],mx[now<<1|1]);
}
inline void apply(CI now,CI mv)
{
mx[now]+=mv; tag[now]+=mv;
}
inline void pushdown(CI now)
{
if (tag[now]) apply(now<<1,tag[now]),apply(now<<1|1,tag[now]),tag[now]=0;
}
public:
#define TN CI now=1,CI l=1,CI r=M
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void modify(CI beg,CI end,CI mv,TN)
{
if (beg<=l&&r<=end) return apply(now,mv);
int mid=l+r>>1; pushdown(now);
if (beg<=mid) modify(beg,end,mv,LS);
if (end>mid) modify(beg,end,mv,RS);
pushup(now);
}
inline int query(void)
{
return mx[1];
}
#undef TN
#undef LS
#undef RS
}SEG;
inline void update(CI x,CI y)
{
// printf("upt %d %d\n",x,y);
if (x>0) SEG.modify(x+1,M,y); else SEG.modify(1,-x,y);
}
struct stack_of_stacks
{
vector <int> stk[N]; int top;
inline void insert(CI x)
{
if (top==0||stk[top].back()<x)
{
stk[++top].push_back(x);
update(x,1);
return;
}
int l=1,r=top,mid,res;
while (l<=r)
if (stk[mid=l+r>>1].back()>=x) res=mid,r=mid-1; else l=mid+1;
update(stk[res].back(),-1);
stk[res].push_back(x);
update(x,1);
}
inline void erase(CI x)
{
update(x,-1);
int l=1,r=top,mid,res;
while (l<=r)
if (stk[mid=l+r>>1].back()>=x) res=mid,r=mid-1; else l=mid+1;
stk[res].pop_back();
if (!stk[res].empty()) update(stk[res].back(),1);
if (res==top&&stk[res].empty()) --top;
}
}L,R;
int main()
{
scanf("%d%d",&n,&q);
for (RI i=1;i<=n;++i)
scanf("%d",&a[i]);
L.insert(a[1]);
for (RI i=n;i>=2;--i)
R.insert(-a[i]);
int pos=1;
while (q--)
{
char opt[5]; scanf("%s",opt);
if (opt[0]=='<')
{
R.insert(-a[pos]);
L.erase(a[pos--]);
} else if (opt[0]=='>')
{
L.insert(a[++pos]);
R.erase(-a[pos]);
} else
{
int v; scanf("%d",&v);
L.erase(a[pos]);
L.insert(a[pos]=v);
printf("%d\n",SEG.query());
}
}
return 0;
}
G. Game of Darts
签到,暴搜即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstdlib>
using namespace std;
vector <int> score;
int P;
inline void DFS(int now,int sum,vector <int> ans=vector <int>())
{
if (sum==P)
{
int last=ans.back();
if (last%2==1) return;
if (last==50||last/2>=1&&last/2<=20)
{
puts("YES");
printf("%d\n",now);
for (auto x:ans) printf("%d ",x);
putchar('\n');
exit(0);
}
}
if (now>=3) return;
for (auto x:score)
{
ans.push_back(x);
DFS(now+1,sum+x,ans);
ans.pop_back();
}
}
int main()
{
score.push_back(50);
score.push_back(25);
for (int a=1;a<=20;++a)
{
score.push_back(a);
score.push_back(2*a);
score.push_back(3*a);
}
scanf("%d",&P);
if (P==0) return puts("NO"),0;
DFS(0,0);
return puts("NO"),0;
}
H. Hiking
令 \(mn=\min a_i,mx=\max a_i\),首先考虑 \(mx<2mn\) 的情形
有个很显然的构造法就是先把所有数分别取一个递增排列,然后再在剩下还有的数中每个取一个递增排列,直到将所有数用完
这样构造出的答案为所有数出现次数的最大值 \(cnt\),这显然是答案的下界
但是当 \(mx=2mn\) 时,这样做就会不可避免的出现把 \(mx\) 放在 \(mn\) 前面的情况,此时会产生 \(2\) 的贡献
为了尽量避免这种情形,我们让 \(mn\) 尽量出现在靠前的那些序列中,让 \(mx\) 出现在尽量靠后的序列中即可
#include<cstdio>
#include<iostream>
#include<map>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1e6+5;
int t,n,a[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
map <int,int> mp;
// vector <int> vec;
for (int i=1;i<=n;++i)
{
scanf("%d",&a[i]); ++mp[a[i]];
// vec.push_back(x);
}
int mn=*min_element(a+1,a+n+1),mx=*max_element(a+1,a+n+1),mx_cnt=0;
if (mn==mx)
{
printf("%d\n",n-1);
for (int i=1;i<=n;++i)
printf("%d%c",mn," \n"[i==n]);
continue;
}
for (auto [val,c]:mp) mx_cnt=max(mx_cnt,c);
int cnt_mn=mp[mn],cnt_mx=mp[mx];
mp.erase(mn); mp.erase(mx);
vector <int> ans;
for (int c=1;c<=mx_cnt;++c)
{
vector <int> rmv,seq;
if (mx_cnt-c+1<=cnt_mn) seq.push_back(mn);
for (auto &[val,c]:mp)
{
seq.push_back(val);
if (--c==0) rmv.push_back(val);
}
if (c<=cnt_mx) seq.push_back(mx);
for (auto val:rmv) mp.erase(val);
reverse(seq.begin(),seq.end());
for (auto x:seq) ans.push_back(x);
}
reverse(ans.begin(),ans.end());
int sum=0;
for (int i=1;i<(int)ans.size();++i)
sum+=ans[i-1]/ans[i];
// sort(vec.begin(),vec.end());
// int inc_sum=0;
// for (int i=1;i<(int)vec.size();++i)
// inc_sum+=vec[i-1]/vec[i];
// if (inc_sum<sum) sum=inc_sum,ans=vec;
printf("%d\n",sum);
for (auto x:ans) printf("%d ",x);
putchar('\n');
}
return 0;
}
I. Identical Fences
注意到题目允许有一定损失率,我们考虑以下启发式算法
把序列每 \(H\) 个位置划分为一组,我们只考虑在每一组内部找到一个最优划分的方法,最后把所有组的答案顺次拼接即可
根据数据生成规则,本机跑了下 \(H=12\) 的情况就可以得到 \(88\%\) 左右的正确率了
当然对于每组直接暴力跑出答案肯定是不行的,因此需要本机先把 \(2^H\) 种情况的最优解的表给打出来
#include <bits/stdc++.h>
constexpr int H = 12;
int n;
std::string s;
extern std::pair<int, int> hkr[1 << H];
void prep() {
for(int i = 0; i < (1 << H); ++i) {
for(int mk1 = 1; mk1 < (1 << H); ++mk1) {
for(int mk2 = 1; mk2 < (1 << H); ++mk2) {
if(mk1 & mk2) continue;
if(__builtin_popcount(mk1) != __builtin_popcount(mk2)) continue;
static std::vector<int> v1, v2;
v1.clear(), v2.clear();
for(int k = 0; k < H; ++k) {
if(mk1 >> k & 1) v1.emplace_back(i >> k & 1);
if(mk2 >> k & 1) v2.emplace_back(i >> k & 1);
}
if(v1 != v2) continue;
if(__builtin_popcount(mk1) > __builtin_popcount(hkr[i].first))
hkr[i] = {mk1, mk2};
}
}
}
std::cerr << "std::pair<int, int> hkr[1 << H] = {\n";
int total = 0;
for(int i = 0; i < (1 << H); ++i) {
std::cerr << " /* i = " << i << " */ {" << hkr[i].first << ", " << hkr[i].second << "},\n";
total += 2 * __builtin_popcount(hkr[i].first);
}
std::cerr << "};\n// Precision: " << (double)total / (double)((1 << H) * H) << std::endl;
}
int main() {
std::ios::sync_with_stdio(false);
// prep();
std::cin >> n;
std::cin >> s;
// n = 100000;
// s.resize(n);
// std::mt19937 rng(78934);
// for(int i = 0; i < n; ++i) s[i] = char('0' + rng() % 2);
// std::cerr << "[s] " << s << std::endl;
std::vector<int> ans1, ans2;
for(int i = 0; i + H <= n; i += H) {
int mk = 0;
for(int j = H - 1; j >= 0; --j) mk = (mk << 1) | (s[i + j] == '1');
auto [mk1, mk2] = hkr[mk];
for(int j = 0; j < H; ++j) {
if(mk1 >> j & 1) ans1.emplace_back(i + j);
if(mk2 >> j & 1) ans2.emplace_back(i + j);
}
}
std::cout << (int)ans1.size() << char(10);
for(int i = 0; i < (int)ans1.size(); ++i) assert(s[ans1[i]] == s[ans2[i]]);
for(int i = 0; i < (int)ans1.size(); ++i) std::cout << ans1[i] << char(i == (int)ans1.size() - 1 ? 10 : 32);
for(int i = 0; i < (int)ans2.size(); ++i) std::cout << ans2[i] << char(i == (int)ans2.size() - 1 ? 10 : 32);
// std::cerr << "Precision: " << (double)(ans1.size() * 2) / n << std::endl;
return 0;
}
std::pair<int, int> hkr[1 << H] = {
// ......
// 此处为打表内容,具体可以见 https://qoj.ac/submission/1936838
}
J. Joyful Guided Tour
考虑用 local search 的思想构造答案
先随机一组初始解,若图中存在同色的 P3,则将中间那个点的颜色修改为其所有邻居中出现次数最少的那个
要证明这个算法最终一定能正确且快速地终止,考虑势能分析
定义某个局面的势能为图中相邻且同色的点对数目,不难发现一次修改会破坏掉至少 \(2\) 个这样的点对,并增加至多 \(1\) 个这样的点对
(因为每个点度数不超过 \(7\),根据鸽笼原理出现次数最少的颜色出现的次数至多为 \(1\))
而初始局面的势能至多为图中总边数 \(7n\),因此这个过程是可以线性时间得到一组合法解的
实现就非常 trivial 了,拿个队列模拟一下即可
#include <bits/stdc++.h>
constexpr int $n = 1'000'006;
std::mt19937 rng(114514);
int n, m;
int cl[$n];
bool enq[$n];
std::vector<int> out[$n];
std::queue<int> q;
void push(int u) {
if(enq[u]) return ;
int cnt = 0;
for(auto out: out[u]) cnt += (cl[out] == cl[u]);
if(cnt < 2) return ;
q.push(u), enq[u] = true;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin >> n >> m;
for(int i = 0, u, v; i < m; ++i) {
std::cin >> u >> v;
out[u].emplace_back(v);
out[v].emplace_back(u);
}
for(int i = 1; i <= n; ++i) cl[i] = std::uniform_int_distribution{1, 4}(rng);
for(int i = 1; i <= n; ++i) push(i);
while(q.size() > 0) {
int hd = q.front(); q.pop(); enq[hd] = false;
static int cnt[5]; memset(cnt, 0, sizeof(cnt));
for(auto out: out[hd]) cnt[cl[out]] += 1;
int nc = std::min_element(cnt + 1, cnt + 5) - cnt;
cl[hd] = nc; push(hd);
for(auto out: out[hd]) push(out);
}
for(int i = 1; i <= n; ++i) std::cout << cl[i] << char(i == n ? 10 : 32);
return 0;
}
K. Key Properties
队友开场写的,我题都没看
#include<bits/stdc++.h>
using namespace std;
int gcd(int a, int b) {
return 0==b ? a : gcd(b, a%b);
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int n; cin >> n;
if (n%2 != 0) {
cout << "NO\n";
} else {
cout << n / 2 * 3 << '\n';
int x;
for (x=2; x <= n/2; ++x) {
if (1 == gcd(x, n/2)) break;
}
for (int i=0; i<n/2; ++i) {
cout << (i*x % (n/2) + 1) << ' ' << ((i*x+x) % (n/2) + 1) << '\n';
}
for (int i=1; i<=n/2; ++i) {
cout << i << ' ' << n/2 + i << '\n';
}
for (int i=n/2+1; i<n; ++i) cout << i << ' ' << i + 1 << '\n';
cout << n << ' ' << n/2+1 << '\n';
}
return 0;
}
L. Letters on T-shirts
签到,我题都没看
#include<bits/stdc++.h>
using namespace std;
bool vis[3];
string str[3];
bool dfs(string ss, vector<int> vec) {
if ("cerc" == ss) {
cout << "YES\n";
cout << vec.size() << '\n';
for (int x : vec) cout << x+1 << ' ';
cout << '\n';
return true;
}
for (int i=0; i<3; ++i) if (!vis[i]){
vis[i] = true; vec.push_back(i);
string nstr = ss + str[i];
// printf("dfs %d\n", i);
if (dfs(nstr, vec)) return true;
vis[i] = false; vec.pop_back();
}
return false;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> str[0] >> str[1] >> str[2];
if (!dfs(string(), vector<int>())) cout << "NO\n" ;
return 0;
}
Postscript
感觉长时间不训练思维水平反而不降反升,再一次印证了我的训练降智论

浙公网安备 33010602011771号