2023 United Kingdom and Ireland Programming Contest (UKIEPC 2023)
Preface
最坐牢的一集,前3h狂出10题然后最后2h坐牢,祁神写J怒调2h最后喜提MLE
赛后我Rush了个H的做法常数太大又喜提TLE,评价是不如去写E题的模拟退火
A. Assessment Disruption
挺精巧的构造题,刚开始以为是构造数据不卡掉想歪了好久,后面仔细一想还是挺简单的
我们将原序列分成两块,前\(\frac{n}{2}\)个元素放的全是两两之间不能比较的,而后\(\frac{n}{2}\)个元素只要从小到大放上可比较的即可(要求后半部分的所有元素均大于前半部分)
不难发现这样每次只会在后半部分删除一个元素,因此保底有\(\frac{n}{2}\)轮
然后每一轮前\(\frac{n}{2}\)个数可以卡满比较次数,大致为\(\frac{n}{2}\times \frac{n}{2}\times \frac{1}{2}\),因此总体来看可以做到\(\frac{n^3}{16}\)次比较,可以通过此题
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005;
int n,m,W,w[N];
int main()
{
RI i; for (scanf("%d%d",&n,&W),m=n>>1,i=1;i<=n;++i)
if (W-i>=0) w[i]=W-i; else if (W+i<=10000) w[i]=W+i;
for (i=1;i<=n-m;++i) printf("%d %d\n",w[m+i],i);
for (i=1;i<=m;++i) printf("%d %d\n",w[m-i+1],n-m+i);
return 0;
}
B. Boat Commuter
签到题,对于每张卡模拟一下即可(妈的这题刚开始看错题了WA了一发后红温了好久)
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,m,k,x,y,lst[N],ans[N];
int main()
{
RI i,j; for (scanf("%d%d%d",&n,&m,&k),i=1;i<=k;++i)
{
scanf("%d%d",&x,&y);
if (lst[y]==0) lst[y]=x,ans[y]+=100; else
{
ans[y]+=abs(x-lst[y]);
if (x!=lst[y]) ans[y]-=100; lst[y]=0;
}
}
for (i=1;i<=m;++i) printf("%d ",ans[i]);
return 0;
}
C. Clearing Space
ORZ祁神秒了此题
注意到我们可以先枚举钦定一个点强制选,然后从这个点向后可以设计一个DP状态
\(f_{i,j}\)表示向后考虑了\(i\)个点,其中一共选了\(j\)个点时得到的最大面积,转移的话枚举下上次选的是哪个点即可
总复杂度\(O(n^4)\),因为常数极小因此可以跑过
#include<bits/stdc++.h>
#define LD long double
using namespace std;
const LD eps = 1e-8;
int sgn(LD x){return (fabs(x)<=eps ? 0 : (x>eps ? 1 : -1));}
const int N = 105;
struct Pt{
LD x, y;
// Pt operator-(Pt b)const{return Pt{x-b.x, y-b.y};}
// Pt operator+(Pt b)const{return Pt{x-b.x, y-b.y};}
LD crs(Pt b)const{return x*b.y-y*b.x;}
}pt[N];
const LD R = 1000;
const LD PI = acos(-1.0L);
int n, p;
LD A[N];
LD dp[N][N];
signed main(){
cout << setiosflags(ios::fixed) << setprecision(10);
cin >> n;
cin >> p;
for (int i=0; i<n; ++i){
cin >> A[i];
pt[i].x = R*cos(1.0L*A[i]/180*PI);
pt[i].y = R*sin(1.0L*A[i]/180*PI);
}
LD ans = 0.0L;
for (int x=0; x<n; ++x){
dp[0][0] = 0;
for (int i=1; i<=n; ++i){
for (int k=1; k<=min(i, p); ++k){
dp[i][k] = 0.0L;
for (int j=k-1; j<i; ++j){
dp[i][k] = max(dp[i][k], dp[j][k-1] + pt[(x+j)%n].crs(pt[(x+i)%n]));
}
}
}
ans = max(ans, dp[n][p]);
}
cout << ans*0.5L << endl;
return 0;
}
D. Delivery Forces
签到,不难发现最优策略就是拿一个最小值和两个最大值配对
#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int n,a[N]; long long ans;
int main()
{
ios::sync_with_stdio(false); cin.tie(0);
RI i,j; for (cin>>n,i=1;i<=n;++i) cin>>a[i];
for (sort(a+1,a+n+1),i=1,j=n-1;i<=n/3;++i,j-=2) ans+=a[j];
return printf("%lld",ans),0;
}
E. Enchanted Fortress
比赛的时候徐神提出了很多乱搞做法,但由于机时没有去写
后面看题解发现是个meet in middle或者退火,但都没想清楚怎么写,先坑着
F. Fast Forward
被徐神一眼秒了的题
首先可以用双指针求出每首歌后面第一次插入广告的位置
不难发现这个东西可以套个倍增在外面,表示插入\(2^j\)个广告后的位置,然后就做完了
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr int $n = 2000000 + 10;
int n, c;
int d[$n];
int nxt[$n][21];
int main(void) {
std::ios::sync_with_stdio(false);
std::cin >> n >> c;
for(int i = 1; i <= n; ++i) std::cin >> d[i], d[i + n] = d[i];
{
int sum = 0;
for(int i = 1; i <= n; ++i) sum += d[i];
if(sum <= c) {
for(int i = 1; i <= n; ++i) std::cout << '0' << char(i == n ? 10 : 32);
return 0;
}
}
int l = 1, r = 1, sum = 0, dn = n << 1;
while(l <= dn) {
while(r <= dn && sum < c) sum += d[r++];
nxt[l][0] = r;
sum -= d[l++];
}
for(int j = 0; j <= 20; ++j) nxt[dn + 1][j] = dn + 1;
for(int i = dn; i; --i) for(int j = 1; j <= 20; ++j)
nxt[i][j] = nxt[nxt[i][j - 1]][j - 1];
for(int i = 1; i <= n; ++i) {
int cur = i, ans = 0;
for(int j = 20; j >= 0; --j) {
if(nxt[cur][j] - i < n) {
ans |= 1 << j;
cur = nxt[cur][j];
}
}
std::cout << ans << char(i == n ? 10 : 32);
}
return 0;
}
G. Glacier Travel
又又又又又又被徐神秒了的题
首先当两点在同一线段上移动时显然不是最小值,因此我们总可以拆出两点在不同线段上移动的情况
注意到此时两者之间的距离关于时间是单峰函数,因此可以三分求最小值(证明的话考虑用速度的合成,问题等价于一条直线到某个点的距离,这个显然是单峰的)
但实际写起来感觉细节很多,但徐神好像没一会就写完一遍过了
#include <bits/stdc++.h>
using real = long double;
struct vivi {
real x, y;
inline vivi(real x = 0.0L, real y = 0.0L): x(x), y(y) {}
inline vivi operator +(const vivi &b) const { return vivi {x + b.x, y + b.y}; }
inline vivi operator -(const vivi &b) const { return vivi {x - b.x, y - b.y}; }
inline vivi operator *(const real &b) const { return vivi {x * b, y * b}; }
inline real len2() const { return x * x + y * y; }
inline real len() const { return std::sqrt(x * x + y * y); }
inline real dist(const vivi &b) const { return (*this - b).len(); }
inline friend std::istream& operator >> (std::istream& in, vivi &v) {
return in >> v.x >> v.y;
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cout << std::fixed << std::setprecision(10);
int n; real s;
std::cin >> s >> n;
std::vector<vivi> p(n);
for(auto &[x, y]: p) std::cin >> x >> y;
auto res = [&] (size_t st, real offset) {
return (p[st + 1] - p[st]).len() - offset;
};
auto fwd = [&] (size_t st, real offset) {
auto ll = (p[st + 1] - p[st]).len();
return p[st] + (p[st + 1] - p[st]) * (offset / ll);
};
size_t as = 0, bs = 0;
real af = 0.0L, bf = 0.0L;
while(as < n - 1) {
real r = res(as, af);
if(s > r) {
s -= r;
as += 1;
af = 0;
} else {
af = s;
break;
}
}
if(as == n - 1) {
std::cout << (p.back() - p.front()).len() << char(10);
return 0;
}
real ans = 1e30L;
while(as < n - 1) {
// std::cerr << as << ' ' << af << ' ' << bs << ' ' << bf << char(10);
real ar = res(as, af), br = res(bs, bf);
real l = 0, r = std::min(ar, br);
while(r - l > 1e-8) {
real m1 = (l + l + r) / 3, m2 = (l + r + r) / 3;
vivi ap1 = fwd(as, af + m1);
vivi ap2 = fwd(as, af + m2);
vivi bp1 = fwd(bs, bf + m1);
vivi bp2 = fwd(bs, bf + m2);
if((ap1 - bp1).len() < (ap2 - bp2).len()) r = m2;
else l = m1;
}
vivi ap = fwd(as, af + l);
vivi bp = fwd(bs, bf + l);
ans = std::min(+ans, real((ap - bp).len()));
if(ar < br) {
bf += ar;
as += 1;
af = 0;
} else {
af += br;
bs += 1;
bf = 0;
}
}
std::cout << ans << char(10);
return 0;
}
H. History in Numbers
很裸的一个线段树维护奇奇怪怪的东西的题,但我的写法好像常数太大了就是跑不过
不妨用线段树来维护一个区间内所有有用的信息,朴素的想法是要维护以下三者:
- 该区间是否合法
- 该区间在去重后得到的序列
- 该区间在去重后所有minimum元素构成的序列
但稍作思考我们会发现后两者完全不需要知道整个序列的值,只需要知道至多左边两个、右边两个元素的值即可
因此这题的难点就在于合并区间,耐心讨论下即可,最后修改套个Lazy Tag即可
估计可能是我操作的时候全部大力用vector来搞了因此常数巨大,但后面换了deque后又喜提MLE直接给我整不会了
这里扔一份能过对拍正确性没问题的代码供参考
#include<cstdio>
#include<iostream>
#include<vector>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=300005,INF=1e18;
int n,a[N],m;
class Segment_Tree
{
private:
struct segment
{
bool ok; int tag; vector <int> num,mi;
inline void init(void)
{
ok=1; tag=0; num.clear(); mi.clear();
}
friend inline segment operator + (segment A,segment B)
{
segment C; C.init(); C.ok=A.ok&&B.ok;
if (!C.ok) return C;
int LL=A.num.size()>1?A.num[A.num.size()-2]:INF;
int RR=B.num.size()>1?B.num[1]:INF;
int L=A.num.back(),R=B.num[0];
if (L==R)
{
if (LL==INF&&RR==INF)
{
C.num.push_back(L); return C;
}
B.num.erase(B.num.begin());
if (B.mi.size()&&B.mi[0]==R) B.mi.erase(B.mi.begin());
if (L<LL&&L<RR)
{
if (A.mi.empty()||A.mi.back()!=L) A.mi.push_back(L);
}
} else
{
if (A.mi.empty()||A.mi.back()!=L)
{
if (L<LL&&L<R)
{
A.mi.push_back(L);
if (A.mi.size()>=2&&A.mi[A.mi.size()-2]>=A.mi.back()) C.ok=0;
}
} else
{
if (!(L<LL&&L<R)) A.mi.pop_back();
}
if (B.mi.empty()||B.mi[0]!=R)
{
if (R<L&&R<RR)
{
B.mi.insert(B.mi.begin(),R);
if (B.mi.size()>=2&&B.mi[0]>=B.mi[1]) C.ok=0;
}
} else
{
if (!(R<L&&R<RR)) B.mi.erase(B.mi.begin());
}
}
if (A.mi.size()&&B.mi.size()&&A.mi.back()>=B.mi[0]) C.ok=0;
if (!C.ok) return C;
auto merge=[&](vector <int> A,vector <int> B)
{
vector <int> vec;
for (auto x:A) vec.push_back(x);
for (auto x:B) vec.push_back(x);
if (vec.size()<=4) return vec;
vector <int> nvec;
nvec.push_back(vec[0]); nvec.push_back(vec[1]);
nvec.push_back(vec[vec.size()-2]); nvec.push_back(vec.back());
return nvec;
};
C.num=merge(A.num,B.num); C.mi=merge(A.mi,B.mi);
return C;
}
}O[N<<2];
inline void pushup(CI now)
{
O[now]=O[now<<1]+O[now<<1|1];
}
inline void apply(CI now,CI mv)
{
O[now].tag+=mv;
for (auto& x:O[now].num) x+=mv;
for (auto& x:O[now].mi) x+=mv;
}
inline void pushdown(CI now)
{
if (O[now].tag) apply(now<<1,O[now].tag),apply(now<<1|1,O[now].tag),O[now].tag=0;
}
public:
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void build(TN)
{
if (l==r)
{
O[now].init(); O[now].num.push_back(a[l]); return;
}
int mid=l+r>>1; build(LS); build(RS); pushup(now);
}
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 segment query(CI beg,CI end,TN)
{
if (beg<=l&&r<=end) return O[now]; int mid=l+r>>1; pushdown(now);
if (end<=mid) return query(beg,end,LS);
if (beg>mid) return query(beg,end,RS);
return query(beg,mid,LS)+query(mid+1,end,RS);
}
inline bool Query(CI l,CI r)
{
return query(l,r).ok;
}
#undef TN
#undef LS
#undef RS
}SEG;
signed main()
{
//freopen("H.in","r",stdin); freopen("H.out","w",stdout);
ios::sync_with_stdio(false); cin.tie(0);
RI i; for (cin>>n,i=1;i<=n;++i) cin>>a[i];
for (SEG.build(),cin>>m,i=1;i<=m;++i)
{
string opt; int l,r,x; cin>>opt>>l>>r;
if (opt=="check") cout<<(SEG.Query(l,r)?"YES":"NO")<<endl;
else cin>>x,SEG.modify(l,r,x);
}
return 0;
}
I. International Travel
疑似又是个几何题,但过的人极少鉴定为做不了一点
J. Journey of Recovery
祁神被这题狠狠地腐乳了,比赛时没想清楚去写了个复杂的单调队列做法,后面发现直接建图DFS就行了
这题大致思路就是按照地点画出一些轴,然后每个航班可以看作轴上的一些点,同时将同一个轴上的点按时间顺序排列,并将同一个轴上的相邻的点之间也连上边,在上面跑DFS即可
具体代码留着祁神填坑
K. Kernel Scheduler
思博题,徐神刚开始提了很多繁琐的做法
后面我被B搞红温后仔细想了下发现是个经典trick,我们不妨把图中的边分为两类
- \(u\to v\and u<v\),称为A类边
- \(u\to v\and u>v\),称为B类边
不难发现若仅保留A,B类边的一种则得到的图一定无环,因此保留数量较多的一类边即可
#include <bits/stdc++.h>
int main() {
std::ios::sync_with_stdio(false);
int n, m; std::cin >> n >> m;
std::vector<int> e1, e2;
for(int i = 0, f, t; i < m; ++i) {
std::cin >> f >> t;
(f > t ? &e1 : &e2)->push_back(i);
}
std::vector<int> *e = (e1.size() > e2.size() ? &e1 : &e2);
std::cout << "YES\n" << e->size() << char(10);
for(size_t i = 0; i < e->size(); ++i)
std::cout << (*e)[i] + 1 << char(i == e->size() ? 10 : 32);
return 0;
}
L. Last One Standing
祁神写的一个Easy题,我连题目都没看
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
int h1, d1, t1; cin >> h1 >> d1 >> t1;
int h2, d2, t2; cin >> h2 >> d2 >> t2;
int T1 = (h1 + d2-1)/d2*t2 -t2, T2 = (h2 + d1-1)/d1*t1 -t1;
if (T1 > T2) cout << "player one\n";
else if (T1 < T2) cout << "player two\n";
else cout << "draw\n";
return 0;
}
M. Mini-Tetris 3023
签到讨论题,不难发现拿两个Corner可以和所有的S-tile拼成矩形,然后多余的Corner两两配对即可
#include <bits/stdc++.h>
int main(void) {
int a, b, c;
std::cin >> a >> b >> c;
std::swap(b, c);
int ans = a << 1;
if(b >= 2) {
ans += (c << 1) + 3;
ans += (b - 2 >> 1) * 3;
}
std::cout << ans << char(10);
return 0;
}
N. Naming Wine Bottles
签到题,先给每种瓶子分配一个整形编号,然后把这个编号转化成\(26\)进制的数即可保证命名的唯一性
#include<bits/stdc++.h>
#define LD long double
using namespace std;
const int N = 1e4+5;
int n;
LD A[N];
int idx=0;
string calc(int x){
string str;
while (x>0){
str += 'a'+x%26;
x/=26;
}
return str;
}
signed main(){
map<LD, int> mp;
string str("a");
scanf("%d", &n);
for (int i=1; i<=n; ++i){
scanf("%LfL", &A[i]);
if (!mp.count(A[i])) mp[A[i]] = ++idx;
}
for (int i=1; i<=n; ++i) cout << calc(mp[A[i]]) << '\n';
return 0;
}
Postscript
感觉今天没啥存在感啊,被徐神和祁神带飞了

浙公网安备 33010602011771号