Canadian Computing Olympiad 2020

链接

A B C D E F
- + - +6 + +3

A. A Game with Grundy

直接把每个朋友的视野在 y=Y 上截得的整点区间求出,然后离散化之后扫一遍即可。

B. Exercise Deadlines

题目可以转化成:构造一个排列,满足 pidi,要求 pi 逆序对个数尽可能少。

直接贪心,从后往前贪心取没有被取的数且 di 中最大的。容易证明取更小的会使逆序对个数增加,并且更不容易合法。

最后再求一遍逆序对个数,复杂度 O(nlogn)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#define N 200010
using namespace std;
int f[N],d[N];
set<int>s;
int a[N],n;
void add(int x){for(;x;x-=x&-x) a[x]++;}
int qry(int x){int v=0;for(;x<=n;x+=x&-x) v+=a[x];return v;}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) s.insert(i);
    for(int i=1;i<=n;i++) scanf("%d",&d[i]);
    for(int i=n;i;i--)
    {
        auto p=s.upper_bound(d[i]);
        if(p==s.begin()){puts("-1");return 0;}
        f[i]=*--p,s.erase(p);
    }
    long long ans=0;
    for(int i=1;i<=n;i++) ans+=qry(f[i]),add(f[i]);
    printf("%lld\n",ans);
    return 0;
}

C. Mountains and Valleys

如果没有非树边,答案是两倍边数减去直径边数。

结论:至多只会选一条非树边。这个比较好证,因为如果选了两条,答案至少为 5n3,然而此时直径长度有 n3,所以不如直接跑原树

加上树边之后的策略是:断掉对应路径上的一条树边,最大化直径。

分类讨论直径与环的关系即可

D. Travelling Salesperson

挺妙的题。结论是:以任意点出发的最短路径合法长度都是 n

考虑用增量法构造。假设当前有一条合法路径,要插入点 u,如果当前路径全部同色,那么直接将 u 插入末尾一定合法。否则假设路径异色的分界点为 v,其前驱为 vL 后继为 vR,如果 (u,vL)(u,v) 同色,那么直接将 u 插入 vLv 直接,如果 (u,v)(u,vR) 同色那么插入 vvR 之间。如果都不满足那么 (u,vL),(u,vR)(vL,v),(vR,v) 中的至少一条同色, 插入另一条边即可。

构造就是对结论的证明

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 2010
using namespace std;
int L[N],R[N];
bool g[N][N];char s[N];
void ins(int x,int y){L[R[x]]=y;L[y]=x;R[y]=R[x];R[x]=y;}
int main()
{
    int n;scanf("%d",&n);
    for(int i=2;i<=n;i++)
    {
        scanf("%s",s+1);
        for(int j=1;j<i;j++) if(s[j]=='R') g[i][j]=g[j][i]=1;
    }
    for(int i=1;i<=n;i++)
    {
        int j=i%n+1,t=j;bool hv=true;
        R[i]=j,R[j]=0,L[i]=0,L[j]=i;
        for(j=j%n+1;j!=i;j=j%n+1)
        {
            if(!R[t]){ins(t,j);if(g[t][j]!=g[L[t]][t]) hv=false;else t=j;}
            else if(g[L[t]][j]==g[t][j] || g[t][j]==g[t][R[t]])
            {
                ins(L[t],j);
                while(t!=i && g[L[t]][t]==g[t][R[t]]) t=L[t];
                if(t==i) while(R[t]) t=R[t];
            }
            else{ins(t,j);while(R[t] && g[L[t]][t]==g[t][R[t]]) t=R[t];}
        }
        printf("%d\n",n);
        for(int u=i;u;u=R[u]) printf("%d ",u);
        puts("");
    }
    return 0;
}

E. Interval Collection

首先容易证明,最优集合中一定只有至多两个区间。

分类讨论:

  • 如果不存在无交区间,那么最小的交一定是 r 最小的区间与 l 最大的区间。
    当然 r 相同的区间可能有很多,根据题目要求选最大的即可。l 同理。这部分可以用 multiset 解决。
  • 如果存在区间无交,不妨枚举分界点 p,相当于在 p 左边找一个左端点最大的,右边找一个左端点最小的。
    如果每次取当前需要处理的区间 [l,r] 的中点 mid,那么本质上是问右端点在 [l,mid] 的区间中左端点最小值,[mid+1,r] 同理。可以发现这就是线段树的过程,直接用线段树维护即可。

复杂度 O(nlogn)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
using namespace std;
const int N=1000010,mx=1000000,inf=1000000000;
int max(multiset<int>&x){return x.empty()?-inf:*x.rbegin();}
int min(multiset<int>&x){return x.empty()?inf:*x.begin();}
multiset<int>sl[N],sr[N];
struct node{
    int l,r,s;
    node(int l=-inf,int r=inf):l(l),r(r),s(r-l){}
    node(int l,int r,int s):l(l),r(r),s(s){}
}t[N<<2];
node operator +(node a,node b){return node(max(a.l,b.l),min(a.r,b.r),min({a.s,b.s,b.r-a.l}));}
void update(int p,int u=1,int l=1,int r=mx)
{
    if(l==r){t[u]=node(max(sl[l]),min(sr[l]));return;}
    int mid=(l+r)>>1;
    if(p<=mid) update(p,u<<1,l,mid);
    else update(p,u<<1|1,mid+1,r);
    t[u]=t[u<<1]+t[u<<1|1];
}
inline void erase(multiset<int>&x,int y){x.erase(x.find(y));}
int main()
{
    int q;scanf("%d",&q);
    multiset<int>L,R;
    while(q --> 0)
    {
        char op[3];int l,r;scanf("%s%d%d",op,&l,&r);
        if(op[0]=='A') L.insert(l),R.insert(r),sl[r].insert(l),sr[l].insert(r);
        else erase(L,l),erase(R,r),erase(sl[r],l),erase(sr[l],r);
        update(l),update(r);
        if(max(L)<min(R)) printf("%d\n",min(sr[max(L)])-max(sl[min(R)]));
        else printf("%d\n",t[1].s);
    }
    return 0;
}

F. Shopping Plans

牛逼题。

首先当然组内排序。考虑先处理 xj=yj=1 的部分,也就是说每个组只会选择一个数字。

一种很显然的做法是:维护每个序列指针和一个位置 p 表示当前已经处理到 p 对应组。每次从队列中取出最小值,将 p 对应组往后移一位,或者选择一个 p>p 后移一维,容易证明是不重不漏的。复杂度 O(n2logn)

可以发现这样非常浪费,因为每次都要枚举 p,决策数量 O(n)。思考能不能一次只移动 pp+1

但是直接做是不行的,因为如果我们只让 p=p+1,那么新状态实际上和原先相同。如果钦定 p 对应组移动一步,然而每一组不一定强制移动。

考虑增加一个“反悔”机制,即:如果 p=p+1 然而 p 对应组只移动了恰好一步,我们可以认为这一步是为了转移“被迫的”,在令 p 移动一步的同时撤销 p 的移动。

不过这样要求对于 p>pp 移动第一步一定要比 p 移动第一步要优,因为我们无法在 p 移动第一步之前将 p 放入堆。所以开始时将组按 a2a1 从小到大排序即可。复杂度 O(klogk)

考虑 m=1 的部分,即需要对组内处理。首先枚举所有可行长度 L[xi,yi],将前 L 个字符加入初始队列。
队列中维护 l,r,p,表示当前在修改 p,其中 pl 位置开始,不能超过 r。每次操作可以:将 p 后移一位,或者将 l 设置为 l1,同时 r=p , p=l-1。这样每次只有两种决策,复杂度 O(klogk)

最后对于正解,将每个组当成一个黑箱子,可以发现黑箱子中的操作只有输出下一个的解,这恰好就是 m=1 时求的东西,直接两层 k 短路即可。复杂度 O(klogk)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<queue>
#define N 200010
#define ll long long
#define inf 100000000000000
#define S(x) ((int)x.size())
using namespace std;
vector<int>f[N];int l[N],r[N];
struct sta{
    int l,r,p;ll s;
    sta(int l=0,int r=0,int p=0,ll s=0):l(l),r(r),p(p),s(s){}
    bool operator <(const sta a)const{return s>a.s;}
};
struct node{
    priority_queue<sta>q;
    vector<int>h;
    void init(int u)
    {
        h.swap(f[u]);
        ll s=0;r[u]=min(r[u],S(h));
        if(l[u]==0) q.push(sta());
        for(int i=0;i<r[u];i++)
        {
            s+=h[i];
            if(i+1>=l[u]) q.emplace(i,S(h),i,s);
        }
    }
    vector<ll>res;
    ll operator [](int x)
    {
        while(x>=S(res))
        {
            while(!q.empty() && x>=S(res))
            {
                sta u=q.top();q.pop();
                res.push_back(u.s);
                if(u.p+1<u.r) q.emplace(u.l,u.r,u.p+1,u.s+h[u.p+1]-h[u.p]);
                if(u.p!=u.l && u.l>0) q.emplace(u.l-1,u.p,u.l,u.s+h[u.l]-h[u.l-1]);
            }
            if(x>=S(res)) return inf;
        }
        return res[x];
    }
}g[N];
int id[N];
struct sta2{
    int p,x;ll s;
    sta2(int p=0,int x=0,ll s=0):p(p),x(x),s(s){}
    bool operator <(const sta2 a)const{return s>a.s;}
};
priority_queue<sta2>q;
int main()
{
    int n,m,k;
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1,x,y;i<=n;i++) scanf("%d%d",&x,&y),f[x].push_back(y);
    for(int i=1;i<=m;i++) scanf("%d%d",&l[i],&r[i]),sort(f[i].begin(),f[i].end()),g[i].init(i),id[i]=i;
    cerr<<"start"<<endl;
    sort(id+1,id+m+1,[&](int x,int y){return g[x][1]-g[x][0]<g[y][1]-g[y][0];});
    ll s=0;
    for(int i=1;i<=m;i++) s+=g[i][0];
    if(s>=inf){while(k --> 0) puts("-1");return 0;}
    printf("%lld\n",s);k--;
    q.emplace(1,1,s+g[id[1]][1]-g[id[1]][0]);
    while(k --> 0)
    {
        sta2 u=q.top();q.pop();int i=id[u.p],j=id[u.p+1];
        if(u.s>=inf){puts("-1");while(k --> 0) puts("-1");return 0;}
        printf("%lld\n",u.s);
        q.emplace(u.p,u.x+1,u.s+g[i][u.x+1]-g[i][u.x]);
        if(u.p<m) q.emplace(u.p+1,1,u.s+g[j][1]-g[j][0]);
        if(u.p<m && u.x==1) q.emplace(u.p+1,1,u.s+g[j][1]-g[j][0]-(g[i][1]-g[i][0]));
    }
    return 0;
}
posted @   Flying2018  阅读(152)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示