省选联考 2018 题解

发布时间 2023-03-26 16:07:56作者: gtm1514

感觉有的歌确实不适合中午刚起来脑袋晕晕乎乎的就去听。太舒缓或者太激烈都不太好。太舒缓容易让人想睡回去,比如我今天中午打了半个小时哈欠。太激烈的……想象一下中午如果放 VIS::CRACKED 会怎样。反正要我我会晕一下午。那我得好好思考一下什么东西能卡在这两个界之间。那我寻思着不如来点 Eurobeat。那我想想最近的 Eurobeat 的例子似乎就是 INTERNET OVERDOSE。那算了吧,重复就不太好了。

真想整活的话可以刚个 Virtual to Live 上去。翻看 maimai 中二 音击 的曲库似乎是个好的主意。

思考了一下我有好办法了。脑力。

算了我推俩歌吧。一个 World's end loneliness。一个 И00。

一双木棋 chess

我超上来就博弈论这么刺激吗。遂看题解。wc 诈骗题那没事了。

首先可以发现每个状态都是占满左上角一片的。然后爆搜一下发现状态数 \(184756\)。然后过了。

顺便学习了一下对抗搜索是个什么东西。

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <cstring>
#define int long long
using namespace std;
const int p=11;
int n,m,a[11][11],b[11][11],now[11];
unordered_map<int,int>mp;
int dfs(int x){
    if(mp.find(x)!=mp.end())return mp[x];
    int ret=x;
    for(int i=n;i>=1;i--){
        now[i]=ret%p;ret/=p;
    }
    int num=0;
    for(int i=1;i<=n;i++)num+=now[i];
    if(num&1)ret=0x3f3f3f3f;
    else ret=-0x3f3f3f3f;
    for(int i=1;i<=n;i++){
        if((i==1||now[i-1]>now[i])&&now[i]<m){
            now[i]++;int hs=0;
            for(int j=1;j<=n;j++)hs=hs*p+now[j];
            if(num&1)ret=min(ret,dfs(hs)-b[i][now[i]]);
            else ret=max(ret,dfs(hs)+a[i][now[i]]);
            now[i]--;
        }
    }
    return mp[x]=ret;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++)scanf("%lld",&a[i][j]);
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++)scanf("%lld",&b[i][j]);
    }
    int ret=0;
    for(int i=1;i<=n;i++)ret=ret*p+m;
    mp[ret]=0;
    dfs(0);
    printf("%lld\n",mp[0]);
    return 0;
}

IIIDX

久仰大名。

首先 \(d\) 互不相同的时候有一个显然的贪心。想了想似乎有重复的时候不是很显然。

又想了一会发现直接提前留下一块等到时候再填上就行了。从大到小排序,线段树二分一下,有相同的选最右边的。

又想了想不太知道如何应付多次。发现扫到一个点的时候把父亲的贡献去掉就行了。那我是 sb。

然后写挂 nan 次。好线段树没 pushdown,鉴定为人菜。场切的人不多大概是因为有的细节比较诡异。

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <cstring>
#define lson rt<<1
#define rson rt<<1|1
using namespace std;
int n,size[500010],fa[500010],d[500010],ans[500010],cnt[500010];
double k;
bool vis[500010];
struct node{
    int l,r,min,lz;
}tree[2000010];
void pushup(int rt){
    tree[rt].min=min(tree[lson].min,tree[rson].min);
}
void pushtag(int rt,int val){
    tree[rt].min+=val;tree[rt].lz+=val;
}
void pushdown(int rt){
    if(tree[rt].lz){
        pushtag(lson,tree[rt].lz);pushtag(rson,tree[rt].lz);
        tree[rt].lz=0;
    }
}
void build(int rt,int l,int r){
    tree[rt].l=l;tree[rt].r=r;
    if(l==r){
        tree[rt].min=l;return;
    }
    int mid=(l+r)>>1;
    build(lson,l,mid);build(rson,mid+1,r);
    pushup(rt);
}
void update(int rt,int l,int r,int val){
    if(l<=tree[rt].l&&tree[rt].r<=r){
        pushtag(rt,val);return;
    }
    pushdown(rt);
    int mid=(tree[rt].l+tree[rt].r)>>1;
    if(l<=mid)update(lson,l,r,val);
    if(mid<r)update(rson,l,r,val);
    pushup(rt);
}
int query(int rt,int k){
    if(tree[rt].l==tree[rt].r){
        return tree[rt].min>=k?tree[rt].l:tree[rt].l+1;
    }
    pushdown(rt);
    if(tree[rson].min>=k)return query(lson,k);
    else return query(rson,k);
}
bool cmp(int a,int b){
    return a>b;
}
int main(){
    scanf("%d%lf",&n,&k);
    for(int i=1;i<=n;i++)scanf("%d",&d[i]);
    sort(d+1,d+n+1,cmp);
    for(int i=n-1;i>=1;i--){
        if(d[i]==d[i+1])cnt[i]=cnt[i+1]+1;
    }
    for(int i=n;i>=1;i--){
        fa[i]=i/k;
        size[i]++;size[fa[i]]+=size[i];
    }
    build(1,1,n);vis[0]=true;
    for(int i=1;i<=n;i++){
        if(!vis[fa[i]]){
            update(1,ans[fa[i]],n,size[fa[i]]-1);
            vis[fa[i]]=true;
        }
        int pos=query(1,size[i]);
        pos+=cnt[pos];cnt[pos]++;pos-=cnt[pos]-1;ans[i]=pos;
        update(1,ans[i],n,-size[i]);
    }
    for(int i=1;i<=n;i++)printf("%d ",d[ans[i]]);
    return 0;
}

秘密袭击 coat

感觉好像把原来考试的一个排列题搬到树上。那个题 \(O(nk)\) 的。

然后发现是神题。算了毕竟是 D1T3。

首先套路拆分每个点的贡献。然后人傻了。然而发现这题应该拆每种权值的贡献。

\[\begin{aligned} &\sum_{T\subseteq S}Kth\max(T)\\ =&\sum_{i=1}^Wi\sum_{T\subseteq S}[Kth\max(T)=i]\\ =&\sum_{i=1}^W\sum_{T\subseteq S}[Kth\max(T)\ge i]\\ =&\sum_{i=1}^W\sum_{T\subseteq S}[cnt_i\ge k] \end{aligned} \]

\(cnt_i\)\(i\)\(T\) 中出现次数。

等会再说。

劈配

Yazid 题,但是不是很神秘。

看见这种吊东西一堆限制再加个小数据范围果断网络流。那发现第一问就是个二分图匹配,每次加一个人一档志愿的边跑二分图匹配就行了(一个优化是如果不能匹配就把边删掉,听说不删会 t)。

考虑第二问。这个可以二分一个排名然后把这个排名以前的和自己都连上再跑。这个好像跑不过去,专门给每个前缀存一个处理完这些前缀时候的图在这上边跑好像就行了。

没写。

林克卡特树

神奇 WQS 二分。《题目并不难。》

首先看上去长得就像 dp 所以考虑 dp。然后转化一下问题,割掉 \(k\) 条边剩下 \(k+1\) 个连通块,每个连通块走一遍直径。那么就是选出 \(k+1\) 条链使得和最大。

\(dp_{x,i}\) 为到 \(x\)\(i\) 条链。考虑怎么合并,发现没法合并。那加一维,\(dp_{x,i,0/1/2}\) 为到 \(x\)\(i\) 条链,\(x\) 度数为 \(0/1/2\) 的答案。这个就可以讨论两边度数粘起来:

  1. 方便讨论,每次更新 \(dp_{x,i,0}=\max(dp_{x,i,0},dp_{x,i-1,1},dp_{x,i,2})\)
  2. \(x\) 度数为 \(0\):把子树的最优解合并一下,即 \(dp_{x,i,0}=\max(dp_{x,i,0},dp_{x,j,0}+dp_{v,i-j,0})\)
  3. \(x\) 度数为 \(1\):从自己的链和儿子的链选一个,即 \(dp_{x,i,1}=\max(dp_{x,i,1},dp_{x,j,0}+dp_{v,i-j,1}+w_{x,v},dp_{x,j,1}+dp_{v,i-j,0})\)
  4. \(x\) 度数为 \(2\):自己在一条链中间,或者把自己和儿子粘起来,即 \(dp_{x,i,2}=\max(dp_{x,i,2},dp_{x,j,1}+dp_{v,i-j-1,1}+w_{x,v},dp_{x,j,2}+dp_{v,i-j,0})\)

边界 \(dp_{x,0,0}=dp_{x,0,1}=dp_{x,0,2}=0\),其他 \(-\infty\)。这是 \(O(nk)\) 的做法。

正解?打表发现这个东西是个上凸的(或者猜测每次贪心选最大),那 WQS 二分一下就行了。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define int long long
using namespace std;
int n,K;
struct gra{
    int v,w,next;
}edge[600010];
int t,head[300010];
void add(int u,int v,int w){
    edge[++t].v=v;edge[t].w=w;edge[t].next=head[u];head[u]=t;
}
struct node{
    int x,y;
    bool operator<(const node&s)const{
        return x==s.x?y>s.y:x<s.x;
    }
    node operator+(const node&s)const{
        return{x+s.x,y+s.y};
    }
    node operator+(const int&s)const{
        return{x+s,y};
    }
    node sub(int s){
        return {x-s,y+1};
    }
}dp[300010][3];
void dfs(int x,int f,int mid){
    dp[x][2]=max(dp[x][2],{-mid,1});
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            dfs(edge[i].v,x,mid);
            dp[x][2]=max(dp[x][2]+dp[edge[i].v][0],(dp[x][1]+dp[edge[i].v][1]+edge[i].w).sub(mid));
            dp[x][1]=max(dp[x][1]+dp[edge[i].v][0],dp[x][0]+dp[edge[i].v][1]+edge[i].w);
            dp[x][0]=dp[x][0]+dp[edge[i].v][0];
        }
    }
    dp[x][0]=max(dp[x][0],max(dp[x][1].sub(mid),dp[x][2]));
}
signed main(){
    scanf("%lld%lld",&n,&K);K++; 
    for(int i=1;i<n;i++){
        int u,v,w;scanf("%lld%lld%lld",&u,&v,&w);
        add(u,v,w);add(v,u,w);
    }
    int l=-1e12,r=1e12;
    while(l<r){
        int mid=(l+r)>>1;
        memset(dp,0,sizeof(dp));
        dfs(1,0,mid);
        if(dp[1][0].y<=K)r=mid;
        else l=mid+1;
    }
    memset(dp,0,sizeof(dp));
    dfs(1,0,l);
    printf("%lld\n",dp[1][0].x+l*K);
    return 0;
}

制胡窜

纯恶心人的题。

线段树合并维护 endpos 然后大力分类讨论。过程繁而不难,略去(真的很恶心,真的不想写了)。