杂题瞎做

图论

CF19E Fairy

link

瞎做的第一题。。。古早 *2900,是我没有想出来的捏,体感 *2400。

先说一下我的心路历程,先看联通的问题,对于联通块,发现有超过两个联通块不是二分图无解,然后全是二分图的话,也显然随便删,感性理解。接着看到二分图,于是考虑转化为没有奇环,所以显然的建 dfs 树,然后用树上差分打标记即可。之后手模样例发现这是错的,遂放弃。先说一下错误的点,你考虑一种显然的反例,就是一个偶环包着一个奇环,此时删除奇环上的树边没有意义。

后来就想到,既然枚举奇环贡献边很复杂,不如对于每条边都试着做做。一种显然的方法,分讨树边和非树边,然后删除非树边显然,当且仅当只有这一个非树边会造成奇环,否则啥用没有,树边的探讨,一个显然的想法是必然是各个奇环的交,但是只有这一个限制太弱了,接着就不会了。。。

经过提示,貌似会了。我们来换个角度看问题,奇环是不好想的,于是考虑用二分图判定的另一种形式——染色。我们先把 dfs 树先都染上色,对于奇环来讲就可以转化成非树边连了同色点。然后来用染色刻画一下断边这个操作,显然只断边不会解决连接同色的情况,也就是说我们断边的本质是把分出去的那个子树的全部颜色做了一次翻转。然后此时来思考,我的更强限制也就出现在,翻转这棵子树后,又有非树边构成同色,因此也就是对于一条可以删的树边,它不能出现在本来的连了异色的的边和树边锁构成的环里,否则就会把这条边变成连接同色的环。

于是这题做完了。

纯净版压行版:

#include<bits/stdc++.h>
using namespace std;
const int N=10005;
struct edge{
    int to,nxt,id;
}e[N<<1];
int hd[N],tot=1,col[N],d[N],f[N],sz[N],w[N],top[N],vis[N];
int c1[N],c2[N],sum,pos,ans[N],cnt;
void add(int u,int v,int id){
    e[++tot].to=v;
    e[tot].nxt=hd[u];
    e[tot].id=id;
    hd[u]=tot;
}
void dfs1(int u,int fa){
    vis[u]=1;
    sz[u]=1,d[u]=d[fa]+1,f[u]=fa;
    int mx=0,k=0;
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (!vis[v]){
            col[v]=col[u]^1;
            dfs1(v,u);
            sz[u]+=sz[v];
            if (sz[v]>mx)mx=sz[v],k=v;
        }
    }
    w[u]=k;
}
void dfs2(int u){
    if (!w[u])return;
    top[w[u]]=top[u];
    dfs2(w[u]);
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (f[v]!=u||v==w[u])continue;
        top[v]=v;
        dfs2(v);
    }
}
int lca(int u,int v){
    while (top[u]!=top[v]){
        if (d[top[u]]>d[top[v]])u=f[top[u]];
        else v=f[top[v]];
    }
    return (d[u]>d[v]?v:u);
}
void dfs3(int u,int pid){
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to,id=e[i].id;
        if (id==pid)continue;
        if (d[v]==d[u]+1){
            dfs3(v,id);
            c1[u]+=c1[v];
            c2[u]+=c2[v];
        }
    }
    if (pid&&c1[u]==sum&&c2[u]==0)ans[++cnt]=pid;
}
int main(){
    ios::sync_with_stdio(0);cin.tie(0);
    int n,m;cin>>n>>m;
    for (int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        add(u,v,i),add(v,u,i);
    }
    for (int i=1;i<=n;++i)if (!vis[i]){
        col[i]=0;
        dfs1(i,0);
        top[i]=i;
        dfs2(i);
    }
    for (int u=1;u<=n;++u){
        for (int i=hd[u];i;i=e[i].nxt){
            int v=e[i].to,id=e[i].id;
            if (d[v]<d[u]&&f[u]!=v){
                if (col[u]==col[v]){
                    sum++;
                    pos=id;
                    c1[u]++,c1[v]++,c1[lca(u,v)]-=2;
                }
                else c2[u]++,c2[v]++,c2[lca(u,v)]-=2;
            }
        }
    }
    for (int i=1;i<=n;++i)if (d[i]==1)dfs3(i,0);
    if (!sum){
        cout<<m<<'\n';
        for (int i=1;i<=m;++i)cout<<i<<" ";
        return 0;
    }
    if (sum==1)ans[++cnt]=pos;
    sort(ans+1,ans+1+cnt);
    cout<<cnt<<'\n';
    for (int i=1;i<=cnt;++i)cout<<ans[i]<<" ";
}

额注释版

#include<bits/stdc++.h>
using namespace std;
const int N=10005;

// 前向星存图
struct edge{
    int to,nxt,id;   // id 是边的原始编号
}e[N<<1];
int hd[N],tot=1;     // tot=1 方便双向边配对(i^1 取反向边,虽然本题没用到)

// 树剖相关
int col[N];          // 染色 0/1
int d[N],f[N];       // 深度,父亲
int sz[N],w[N];      // 子树大小,重儿子
int top[N];          // 链顶
int vis[N];          // dfs1 的访问标记

// 差分标记
int c1[N],c2[N];     // c1: 奇环覆盖次数,c2: 偶环覆盖次数
int sum;             // 奇环总数
int pos;             // 唯一奇环对应的那条非树边编号
int ans[N],cnt;      // 答案数组

void add(int u,int v,int id){
    e[++tot].to=v;
    e[tot].nxt=hd[u];
    e[tot].id=id;
    hd[u]=tot;
}

// ---------- 树剖第一遍 ----------
// 求 f, d, sz, w,顺带染色
void dfs1(int u,int fa){
    vis[u]=1;
    sz[u]=1, d[u]=d[fa]+1, f[u]=fa;
    int mx=0,k=0;
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (!vis[v]){                    // 树边:儿子
            col[v]=col[u]^1;             // 染色:与父亲相反
            dfs1(v,u);
            sz[u]+=sz[v];
            if (sz[v]>mx)mx=sz[v],k=v;   // 找重儿子
        }
    }
    w[u]=k;                              // 重儿子
}

// ---------- 树剖第二遍 ----------
// 求 top
void dfs2(int u){
    if (!w[u])return;                    // 叶子
    top[w[u]]=top[u];                    // 重儿子继承链顶
    dfs2(w[u]);
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (f[v]!=u||v==w[u])continue;   // 只看轻儿子(父亲是 u 且不是重儿子)
        top[v]=v;                        // 轻儿子新开一条链
        dfs2(v);
    }
}

// ---------- 树剖 LCA ----------
int lca(int u,int v){
    while (top[u]!=top[v]){
        if (d[top[u]]>d[top[v]])u=f[top[u]];
        else v=f[top[v]];
    }
    return (d[u]>d[v]?v:u);
}

// ---------- 差分回溯 ----------
// 只沿树边向下(d[v]==d[u]+1),从叶子往上累加 c1, c2
void dfs3(int u,int pid){
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to,id=e[i].id;
        if (id==pid)continue;
        if (d[v]==d[u]+1){               // 树边
            dfs3(v,id);
            c1[u]+=c1[v];
            c2[u]+=c2[v];
        }
    }
    // 判断这条树边 (u, f[u]) 是否可删
    if (pid&&c1[u]==sum&&c2[u]==0)ans[++cnt]=pid;
}

// ---------- 主函数 ----------
int main(){
    ios::sync_with_stdio(0);cin.tie(0);
    int n,m;cin>>n>>m;
    for (int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        add(u,v,i),add(v,u,i);
    }

    // 1. 建 dfs 树 + 树剖(处理多联通块)
    for (int i=1;i<=n;++i)if (!vis[i]){
        col[i]=0;
        dfs1(i,0);
        top[i]=i;
        dfs2(i);
    }

    // 2. 处理所有非树边,打差分标记
    //    d[v]<d[u] && f[u]!=v 保证每条非树边只由浅到深处理一次
    for (int u=1;u<=n;++u){
        for (int i=hd[u];i;i=e[i].nxt){
            int v=e[i].to,id=e[i].id;
            if (d[v]<d[u]&&f[u]!=v){
                if (col[u]==col[v]){          // 同色 → 奇环
                    sum++;
                    pos=id;                   // 记录这条非树边
                    c1[u]++,c1[v]++;          // 端点 +1
                    c1[lca(u,v)]-=2;          // LCA -2(差分)
                }
                else {                        // 异色 → 偶环
                    c2[u]++,c2[v]++;
                    c2[lca(u,v)]-=2;
                }
            }
        }
    }

    // 3. 回溯累加差分
    for (int i=1;i<=n;++i)if (d[i]==1)dfs3(i,0);

    // 4. 输出答案
    if (!sum){                                // 原图就是二分图
        cout<<m<<'\n';
        for (int i=1;i<=m;++i)cout<<i<<" ";
        return 0;
    }
    if (sum==1)ans[++cnt]=pos;                // 唯一奇环的非树边也可删
    sort(ans+1,ans+1+cnt);
    cout<<cnt<<'\n';
    for (int i=1;i<=cnt;++i)cout<<ans[i]<<" ";
}

P8346 「Wdoi-6」最澄澈的空与海

link

体感 *2100。

小清新,画一下图,发现如果每个点的度数都 \(\ge 2\),那么必然可以画出两种以上,然后此时有一种想法就是,要有度数为 \(1\) 的点,但是有几个不好刻画,那么我们就有一种类似于拓扑排序的想法,把所有的度数为 \(1\) 的点找出来,连点带边删掉,也就是先把突破口匹配掉,因为这个点死了所以与其相连的那个点也要配掉。然后继续找,发现这样找下去如果点都没了那么就是合法的,如果中间有一部分删不掉就不合法了。

#include<bits/stdc++.h>
using namespace std;
const int N=4e6+5;
struct egde{
    int to,nxt;
}e[N*2];
int hd[N],tot;
void add(int u,int v){
    e[++tot].to=v;
    e[tot].nxt=hd[u];
    hd[u]=tot;
}
int d[N],vis[N];
void solve(){
    int n,m;cin>>n>>m;
    n=n*2;
    for (int i=1;i<=n;++i) d[i]=vis[i]=hd[i]=0;
    tot=0;
    for (int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        v+=n/2;
        add(u,v);add(v,u);
        d[u]++;d[v]++;
    }
    queue<int>q;
    for (int i=1;i<=n;++i){
        if (d[i]==1)q.push(i);
    }
    while (!q.empty()){
        int u=q.front();q.pop();
        if (vis[u])continue;
        vis[u]=1;
        int vv=0;
        for (int i=hd[u];i;i=e[i].nxt){
            int v=e[i].to;
            if (!vis[v]){
                vv=v;
                break;
            }
        }
        if (!vv) continue;
        vis[vv]=1;
        for (int i=hd[vv];i;i=e[i].nxt){
            int w=e[i].to;
            if (!vis[w]){
                d[w]--;
                if (d[w]==1)q.push(w);
            }
        }
    }
    int ok=1;
    for (int i=1;i<=n;++i) ok&=vis[i];
    if (ok) cout<<"Renko\n";
    else cout<<"Merry\n";
}
int main(){
    int T;cin>>T;
    while (T--)solve();
}

P5663 [CSP-J 2019] 加工零件

link

体感 *1900。

这两天的博客中出现的第一道简单题。容易发现,我可以在一条边来回的刷,所以就是奇偶性的问题,考虑对 \(L\) 奇偶性分讨,然后维护一下分层图的最短路,判掉到不了 \(1\) 的情况就好了。。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct edge{
    int to,nxt;
}e[N*2];
int tot,hd[N];
void add(int u,int v){
    e[++tot].to=v;
    e[tot].nxt=hd[u];
    hd[u]=tot;
}
int dis[N][2];
int main(){
    int n,m,Q;
    cin>>n>>m>>Q;
    for (int i=1;i<=m;++i){
        int u,v;
        cin>>u>>v;
        add(u,v);add(v,u);
    }
    queue<int>q;
    q.push(1);
    memset(dis,0x3f,sizeof dis);
    dis[1][0]=0;
    while (!q.empty()){
        int u=q.front();q.pop();
        for (int i=hd[u];i;i=e[i].nxt){
            int v=e[i].to;
            if (dis[u][1]+1<dis[v][0]){
                dis[v][0]=dis[u][1]+1;
                q.push(v);
            }
            if (dis[u][0]+1<dis[v][1]){
                dis[v][1]=dis[u][0]+1;
                q.push(v);
            }
        }
    }
    while (Q--){
        int a,L;
        cin>>a>>L;
        if (L%2==1){
            if (dis[a][1]<=L) cout<<"Yes\n";
            else cout<<"No\n";
        } else {
            if (dis[a][0]<=L) cout<<"Yes\n";
            else cout<<"No\n";
        }
    }
}

P9377 [THUPC 2023 决赛] 百合

link

好题!显然是蒟蒻做不出来的!

考虑边数太多,一定要去减少边数,然后点并没有很多,于是拆点,看一下不同位置数这个东西怎么刻画,我们可以刻画成取了 \(k\) 位反后相等,发现我并不能一下确定是哪 \(k\) 位,所以需要按位决策,\((i,j,k)\) 表示前 \(j\) 取了 \(k\) 位反,最终得到 \(i\)。转移是好刻画的。然后发现这个复杂度是不对的,我们可以考虑把长度为 \(0\) 的边单独拿出来,跑一遍 BFS 即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1<<18;
int n,m,s;
long long c[20];
struct edge{
    int to,nxt;
    long long w;
}e[N*2];
int hd[N],tot;
void add(int u,int v,long long w){
    e[++tot].to=v;
    e[tot].w=w;
    e[tot].nxt=hd[u];
    hd[u]=tot;
}
long long dis[N];
int vis[N];
int q[N][3],l,r;
bool f[N][20][20];
priority_queue<pair<long long,int>,vector<pair<long long,int>>,greater<pair<long long,int>>>pq;

void update(int u,long long d){
    if (d<dis[u]){
        dis[u]=d;
        pq.push({d,u});
    }
}

void push(int x,int p,int cnt){
    if (!f[x][p][cnt]){
        f[x][p][cnt]=1;
        q[++r][0]=x;
        q[r][1]=p;
        q[r][2]=cnt;
    }
}

void spfa(int s){
    for (int i=0;i<(1<<n);++i)dis[i]=1e18;
    update(s,0);
    while (!pq.empty()){
        int u=pq.top().second;pq.pop();
        if (vis[u])continue;
        vis[u]=1;
        for (int i=hd[u];i;i=e[i].nxt){
            update(e[i].to,dis[u]+e[i].w);
        }
        l=1,r=0;
        push(u,0,0);
        while (l<=r){
            int x=q[l][0],p=q[l][1],cnt=q[l][2];l++;
            if (p<n){
                push(x,p+1,cnt);
                push(x^(1<<p),p+1,cnt+1);
            }
            else {
                update(x,dis[u]+c[cnt]);
            }
        }
    }
}

signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n>>m>>s;
    for (int i=1;i<=n;++i)cin>>c[i];
    for (int i=1;i<=m;++i){
        int u,v;
        long long w;
        cin>>u>>v>>w;
        add(u,v,w),add(v,u,w);
    }
    spfa(s);
    for (int i=0;i<(1<<n);++i)cout<<dis[i]<<" ";
}

旧址

link

这个题,很厉害啊,考试想的差不多没做出来还是太弱了。显然的一个观察是,其实大的情况分布是偏少的,这是一种感觉,然后考虑什么情况能贡献 \(\ge 2\) 的,不妨先考虑 \(=2\),两个点在补图中度数如果都 $ < \frac{n}{2}$ 是肯定可以的,鸽巢原理。接着我们称这些度数 \(\le \frac{n}{2}\) 的点为大点,我们对所有的大点跑一遍补图最短路即可。可以来估算一下大点的数量,我们知道一张图中的度数和为 \(2m\),令大点有 \(k\) 个,则 \(2m \ge k\times \frac{n}{2}\),显然的移项 $ \frac{4m}{n}\ge k$,同时总点数为 \(n\),忽略常数 \(4\),所以大点最多有 \(\min(n,\frac{m}{n})\) 个,根号平衡,\(n= \sqrt {m}\) 时最大,所以复杂度实际很低。然后可以来说一下补图最短路咋做,考虑你知道一个图的补图,你要求原图上某个点到各个点的最短路,并且是原图远比补图稠密的情况,我们维护一个链表,表示当前还有多少点没有访问过,对于每个点,先把它在补图上的点标记为不能走,然后去遍历这个链表,如果还能连就把它松弛并删除,复杂度是 \(O(n+m)\) 的,这里的 \(m\) 注意是原图边数。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m;
vector<int> e[N];
int d[N],ans[N],dis[N],pre[N],nxt[N],vis[N];
queue<int> q;
void del(int x){
    nxt[pre[x]]=nxt[x];
    pre[nxt[x]]=pre[x];
}
void bfs(int s){
    for(int i=1;i<=n;++i){
        pre[i]=i-1;
        nxt[i]=i+1;
        vis[i]=0;
        dis[i]=-1;
    }
    nxt[0]=1;pre[n+1]=n;
    q.push(s);del(s);dis[s]=0;
    while(!q.empty()){
        int u=q.front();q.pop();
        for(auto v:e[u])vis[v]=1;
        for(int i=nxt[0];i!=n+1;i=nxt[i]){
            if(!vis[i]){
                dis[i]=dis[u]+1;
                q.push(i);del(i);
            }
        }
        for(int v:e[u])vis[v]=0;
    }
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
        d[u]++;d[v]++;
    }
    ans[1]=n*(n-1)/2-m;
    for(int u=1;u<=n;++u){
        if(d[u]<n/2){
            for(auto v:e[u]){
                if(d[v]<n/2&&v<u)ans[2]++;
            }
        }
    }
    for(int i=1;i<=n;++i){
        if(d[i]>=n/2){
            bfs(i);
            for(auto v:e[i]){
                if(dis[v]!=-1&&(d[v]<n/2||v<i))ans[dis[v]]++;
            }
        }
    }
    for(int k=1;k<=n-1;++k)cout<<ans[k]<<" ";
}

丁香之路

link

好题,但是我会了所以应该至少理解难度没有那么高,第一次遇到确实不好想的。

首先有一个很典的性质,就是你发现连 \(i \to j\),不如连 \(i \to i+1 \to i+2 \cdots \to j\),连通性会更好,并且代价和原来是一样的。然后你发现这个东西其实可以从欧拉路径的角度去刻画,也就是 \(s\)\(t\) 的度数为奇数,然后中间的度数全部偶数,这是奇偶性上的限制,我们先考虑把这个限制做出来,找出所有与自己奇偶性不相符的点,然后按照顺序,相邻的连上一条即可,可以证明这么做是最优的,接着考虑连通性的问题,那么考虑我现在的连通性本质上是若干个联通块,你就把相邻的块的左右端点分别连上,跑一个最小生成树即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2505;
int d[N],f[N],bel[N];
int find(int x){
    if (f[x]==x)return x;
    return f[x]=find(f[x]);
}
void join(int x,int y){
    f[find(x)]=find(y);
}
struct edge{
    int u,v,w;
}e[N];
bool cmp(edge x,edge y){
    return x.w<y.w;
}
int main(){
    int n,m,s;cin>>n>>m>>s;
    for (int i=1;i<=n;++i)f[i]=i;
    long long sum=0;
    for (int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        d[u]++;d[v]++;sum+=abs(u-v);
        join(u,v);
    }
    for (int i=1;i<=n;++i)bel[i]=find(i);
    for (int t=1;t<=n;++t){
        for (int i=1;i<=n;++i)f[i]=i;
        join(bel[s],bel[t]);
        vector<int>v;
        long long ans=sum;
        for (int i=1;i<=n;++i){
            if ((d[i]&1)^(i==s)^(i==t))v.push_back(i);
        }
        sort(v.begin(),v.end());
        for (int i=1;i<(int)v.size();i+=2){
            for (int k=v[i-1];k<v[i];++k)join(bel[k],bel[v[i]]);
            ans+=v[i]-v[i-1];
        }
        vector<edge>b;
        int pre=0;
        for (int i=1;i<=n;++i){
            if (d[i]||i==s||i==t){
                if (pre&&find(bel[i])!=find(bel[pre]))
                    b.push_back({bel[pre],bel[i],i-pre});
                pre=i;
            }
        }
        sort(b.begin(),b.end(),cmp);
        for (int i=0;i<(int)b.size();++i){
            if (find(b[i].u)!=find(b[i].v)){
                join(b[i].u,b[i].v);
                ans+=b[i].w*2;
            }
        }
        cout<<ans<<" ";
    }
}

DP

镜寺

link

不会先咕着。

染色

link
很厉害的题目。体感 *2300。

我们考虑定义 \(f_i\) 表示在 \(i\) 这个位置,\(i\) 与前一位染不同颜色的最大分数。分析题目性质,因为颜色是相对的,所以此处可以嵌定 \(i\) 染上 \(0\) 色。如果 \(a_i\) 有贡献,显然存在一个 \(j\),使得形如:

j-1 j j+1 .... i-1 i
 0  1  1        1  0

因此枚举 \(j\),此时就可以得到一个暴力转移式:

\[f_i=\max_{1\le j<i}(f_j+\sum_{k=j+1}^{i-1}[a_k=a_{k-1}]a_k+[a_i=a_{j-1}]a_i) \]

\(f_j\) 为前面的贡献,\(\sum_{k=j+1}^{i-1}[a_k=a_{k-1}]a_k\) 是中间全部相同而产生的贡献,以及 \(i\) 自己的贡献 \([a_i=a_{j-1}]a_i\)。直接转移是 \(O(n^3)\) 的、

接着,显然的优化,我们记 \(b_i=[a_i=a_{i-1}]a_i\),然后对 \(b\) 做一个前缀和记为 \(s_i\),则原式可以化为:

\[f_i=\max_{1\le j<i}(f_j+s_{i-1}-s_{j}+[a_i=a_{j-1}]a_i) \]

这样转移是 \(O(n^2)\) 的,然后可以上一些经典技巧。

分离常数:

\[f_i=\max_{1\le j<i}(f_j-s_{j}+[a_i=a_{j-1}]a_i)+s_{i-1} \]

注意到 \(f_j\)\(s_j\) 是相关的,记 \(p_j=f_j-s_j\)于是最值优化,记前面 \(p_j\) 的最大值为 \(mx\),原式化为:

\[f_i=\max(mx,\max_{1\le j<i}([a_i=a_{j-1}]a_i+p_j))+s_{i-1} \]

艾弗森括号不好看,考虑分类讨论拆了,也就是说,两种转移:

  • \(a_{j-1}=a_i\),有一个 \(mx\) 的贡献
  • 不相等,直接就为 \(mx\)

再上一个最值优化,令 \(g_{a_i}=\max_{a_{j-1}=a_{i}} (p_j+a_i)\),则原式化为:

\[f_i=\max(mx,g_{a_i})+s_{i-1} \]

考虑 \(g\) 如何维护,每次更新完成后对 \(p_i+a_{i-1}\)\(\max\) 即可,因为这里的 \(i\) 其实就是作为 \(g\) 定义的 \(j\) 取贡献未来的一个 \(i`\),然后 \(a_{i`}=a_{i-1}\),所以就是这个式子。

注意答案这里,由于我的状态定义是不相同颜色,如果相同颜色输出 \(f_n\) 就寄了,所以我们多整一位叫 \(n+1\) 即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int f[N],a[N],g[N],s[N];
void solve(){
    memset(f,0,sizeof f);
    memset(a,0,sizeof a);
    memset(s,0,sizeof s);
    memset(g,-0x3f,sizeof g);
    int n;cin>>n;
    for (int i=1;i<=n;++i)cin>>a[i];
    for (int i=1;i<=n;++i)s[i]=s[i-1]+(a[i]==a[i-1]?a[i]:0);
    int p=0;
    for (int i=2;i<=n+1;++i){
        f[i]=max(p,g[a[i]])+s[i-1];
        p=max(p,f[i]-s[i]);
        g[a[i-1]]=max(g[a[i-1]],f[i]-s[i]+a[i-1]);
    }
    cout<<f[n+1]<<'\n';
}
signed main(){
    int T;cin>>T;
    while (T--)solve();
}

冰火两重天

link

更厉害的题。体感 *2500。

考虑视作平面直角坐标系上的点,然后并令走冰过的为蓝点,火过的为红点。注意到一个性质,若一个点为蓝点,则其右下方也全部为蓝点,证明是简单的,反证法即可。相对的,若一个点是红点,则其左上方的所有点都为红点。那么也就是我们要找到一条轮廓线,右下方全部用冰过,左上方全部用火过。先排序,把所有点按照横坐标排序,然后考虑一个 DP,\(f_{j}\) 表示处理完前 \(i\) 个点,且所有已确定的蓝点中,最大的 \(y\) 坐标为 \(j\) 的最小花费。分类讨论处理:

  • \(j<y_{i}\),可以全分到红点中,此时 \(f_j=f_j+a_i\);也可以把当前的 \(j\) 拔高到 \(y_i\),也就是 \(f_{y_i}=min_{j<y_i}\{f_j\}+b_i\),当然不变也是可以的。
  • \(j>y_{i}\),只能分到蓝点,也就是 \(f_j=f_j+b_i\)

发现这个东西可以线段树维护,区间加和取区间 \(\min\),于是做完了。对于每个新来的点,先取一下之前的 \(min_{j<y_i}f_j\),然后分别更新直接分点的情况,最后再单独处理拔高的情况即可。

#include<bits/stdc++.h>
#define int long long
#define lc(u) (2*u)
#define rc(u) (2*u+1)
using namespace std;
const int N=3e5+5;
const int INF=1e18;
int f[N];
struct node{
    int x,y,a,b;
}p[N];
bool cmp(node c,node d){
    return c.x<d.x;
}
struct SGT{
    int c[N*4],tag[N*4];
    void pushup(int u){
        c[u]=min(c[lc(u)],c[rc(u)]);
    }
    void build(int u,int l,int r){
        if (l==r){
            c[u]=(l==0?0:INF);
            return;
        }
        int mid=(l+r)/2;
        build(lc(u),l,mid);build(rc(u),mid+1,r);
        pushup(u);
    }
    void maketag(int u,int l,int r,int k){
        tag[u]+=k;
        c[u]+=k;
    }
    void pushdown(int u,int l,int r){
        int mid=(l+r)/2;
        maketag(lc(u),l,mid,tag[u]);
        maketag(rc(u),mid+1,r,tag[u]);
        tag[u]=0;
    }
    int query(int u,int l,int r,int ql,int qr){
        if (ql<=l&&r<=qr)return c[u];
        pushdown(u,l,r);
        int mid=(l+r)/2,ans=4e18;
        if (mid>=ql)ans=min(ans,query(lc(u),l,mid,ql,qr));
        if (mid<qr)ans=min(ans,query(rc(u),mid+1,r,ql,qr));
        return ans;
    }
    void modify(int u,int l,int r,int ql,int qr,int k){
        if (ql<=l&&r<=qr){
            maketag(u,l,r,k);
            return;
        }
        pushdown(u,l,r);
        int mid=(l+r)/2;
        if (mid>=ql)modify(lc(u),l,mid,ql,qr,k);
        if (mid<qr)modify(rc(u),mid+1,r,ql,qr,k);
        pushup(u);
    }
}sgt;
signed main(){
    int n;cin>>n;
    for (int i=1;i<=n;++i)cin>>p[i].x>>p[i].y>>p[i].a>>p[i].b;
    vector<int>v;
    for (int i=1;i<=n;++i)v.push_back(p[i].y);
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());
    for (int i=1;i<=n;++i)p[i].y=lower_bound(v.begin(),v.end(),p[i].y)-v.begin()+1;
    sgt.build(1,0,n);
    sort(p+1,p+n+1,cmp);
    for (int i=1;i<=n;++i){
        int q=sgt.query(1,0,n,0,p[i].y-1);
        sgt.modify(1,0,n,0,p[i].y-1,p[i].a);
        sgt.modify(1,0,n,p[i].y,n,p[i].b);
        int q2=sgt.query(1,0,n,p[i].y,p[i].y);
        if (q+p[i].b<q2)sgt.modify(1,0,n,p[i].y,p[i].y,-q2+q+p[i].b);
    }
    cout<<sgt.query(1,0,n,0,n);
}

线性生物

link

常规题。感觉没有很 DP,偏数学啊。额蒟蒻独立做出来了所以体感 *1900。

我们先令 \(f_x=E_{x \to x+1}\),由期望线性性质易知,\(E_{1\to n+1}=\sum_{i=1}^n f_i\),考虑如何求 \(f_i\),令从 \(i\) 出发的有向边边集为 \(e_i\),先是一个显然的式子:

\[f_i=\frac{1}{d_i+1}+\frac{\sum_{(i,v)\in e_i} (E_{v \to i+1}+1)}{d_i+1} \]

解释一下,前半段是直接走到 \(i+1\) 的期望步数,然后后半段是走返祖边的,首先选到这一条返祖边的概率为 \(\frac{1}{d_u+1}\),然后对于每一种情况,都是走到 \(i+1\),然后还有一步是 \(i \to v\) 这步。

显然可以化简,变成:

\[f_i=1+\frac{\sum_{(i,v)\in e_i} (E_{v \to i+1})}{d_i+1} \]

然后再考虑 \(E\) 的线性性质,看看概念,注意边界,发现就是

\[f_i=1+\frac{\sum_{(i,v)\in e_i} \sum_{j=v}^{i} f_j}{d_i+1} \]

显然的前缀和优化,令前缀和数组为 \(s_i\),原式写为

\[f_i=1+\frac{\sum_{(i,v)\in e_i} s_i-s_{v-1}}{d_i+1} \]

发现我 \(s_i\) 是不知道的,\(s\) 不能出现 \(f_i\) 因为我还不知道,于是把他们都放到一起试试,再推推,先乘一下 \(d_i+1\)

\[(d_i+1)f_i=d_i+1+\sum_{(i,v)\in e_i} s_i-s_{v-1} \]

正好 \(s_i\) 里有 \(f_i\),拿出来:

\[(d_i+1)f_i=d_i+1+\sum_{(i,v)\in e_i} s_{i-1}-s_{v-1}+d_i \cdot f_i \]

发现正好抵消!移项即可

\[f_i=d_i+1+\sum_{(i,v)\in e_i} s_{i-1}-s_{v-1} \]

直接转移即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
const int mod=998244353;
vector<int>e[N];
int f[N],s[N],d[N];
signed main(){
    int id,n,m;cin>>id>>n>>m;
    for (int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        e[u].push_back(v);
        d[u]++;
    }
    for (int i=1;i<=n;++i){
        f[i]=d[i]+1;
        for (auto v:e[i])f[i]+=(s[i-1]%mod-s[v-1]%mod+mod)%mod;
        s[i]=s[i-1]+f[i];
        s[i]%=mod;
    }
    cout<<s[n]%mod;
}

中国象棋

link

是蒟蒻做不出的题。。最讨厌这种超级大分讨。体感 *2000。

心路历程大概是,哪道题先观察了一下,然后注意到每条横线和每条竖线的炮的个数都不改超过 \(3\) 个,然后就想到了我们要对于行一个取限制一个处理的时候顺手做好,顺势的一个 \(dp\)\(f_{i,j,k}\),前 \(i\) 行,\(j\) 列放了一个,\(k\) 列放了 \(2\) 的方案数。不放的转移显然,\(f_{i,j,k}=f_{i-1,j,k}\),接着发现是个分讨,做了一会就寄了不会了。。。遂查看题解。

\(i\) 行放一个炮:

  • 放在有 \(0\) 个炮的列:

\[f_{i,j,k} \mathrel{+}= f_{i-1,j-1,k} \times (m-(j-1)-k) \]

  • 放在有 \(1\) 个炮的列:

\[f_{i,j,k} \mathrel{+}= f_{i-1,j+1,k-1} \times (j+1) \]

\(i\) 行放两个炮:

  • 都放在 \(0\) 列:

\[f_{i,j,k} \mathrel{+}= f_{i-1,j-2,k} \times \binom{m-(j-2)-k}{2} \]

  • 都放在 \(1\) 列:

\[f_{i,j,k} \mathrel{+}= f_{i-1,j+2,k-2} \times \binom{j+2}{2} \]

  • 一个放 \(0\) 列,一个放 \(1\) 列:

\[f_{i,j,k} \mathrel{+}= f_{i-1,j,k-1} \times j \times (m-j-(k-1)) \]

显然了,答案为 \(\sum_{j=0}^{m}\sum_{k=0}^{m-j} f_{n,j,k}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=9999973;
int f[105][105][105];
int C(int x){
    return x*(x-1)/2%mod;
}
signed main(){
    int n,m;cin>>n>>m;
    f[0][0][0]=1;
    for (int i=1;i<=n;++i){
        for (int j=0;j<=m;++j){
            for (int k=0;k<=m-j;++k){
                f[i][j][k]=f[i-1][j][k];
                if (j>=1)f[i][j][k]=(f[i][j][k]+f[i-1][j-1][k+1]*(k+1))%mod;
                if (k>=1)f[i][j][k]=(f[i][j][k]+f[i-1][j][k-1]*(m-j-k+1))%mod;
                if (j>=2)f[i][j][k]=(f[i][j][k]+f[i-1][j-2][k+2]*C(k+2))%mod;
                if (k>=2)f[i][j][k]=(f[i][j][k]+f[i-1][j][k-2]*C(m-j-k+2))%mod;
                if (j>=1&&k>=1)f[i][j][k]=(f[i][j][k]+f[i-1][j-1][k]*(k*(m-j-k+1)))%mod;
            }
        }
    }
    int ans=0;
    for (int j=0;j<=m;++j)for (int k=0;k<=m-j;++k)ans=(ans+f[n][j][k])%mod;
    cout<<ans;
}

数学

NOIP R6 T4

link

感觉很困难,数学太差了花费了亿点点时间攻克。。。

\([l,r]\)\(x\) 的个数为 \(k\),然后可以先考虑 \(k=0\) 时,此时如何重排其实对答案式没有影响的,就是区间重排数乘上原本的不含 \(x\) 的对数,后者显然可以预处理,记为 \(G_x\),前者记为 \(R\),记区间内颜色 \(c\) 出现次数为 \(f_c\),然后显而易见的,有:

\[R=\frac{(r-l+1)!}{\prod_{c \in S}f_c} \]

记分母为 \(W\)\(r-l+1=m\),也就变成:

\[R=Wm! \]

接着考虑 \(k \ge 1\) 的情况,答案可以拆成受影响的贡献+不受影响的贡献,后者可以写作 \(R(G_x-O)\),其中 \(O\) 是我要扣掉的一部分受影响的对数,具体的,我令 \([l,r]\) 左边最近的值为 \(x\) 的位置为 \(p\),右边类似为 \(q\),那么此时扣掉 \(p\)\(q\) 之间的值即可,可以用前缀和维护。

我们考虑一下受影响的部分,这是本题的 hardest。我们继续拆解问题,可以拆成嵌定 \(x\) 位置的方案数乘上剩下数的排法的方案数,也就是 \(H=\frac{(m-k)!}{\prod_{c\neq x}f_c}\),我想用 \(W\) 表示,那么也就是 \(H=(m-k)!Wk!\)

现在的问题就是求解嵌定 \(k\)\(x\) 的方案,分类讨论,首先是完全位于区间内的贡献,考虑枚举区间长度 \(t\),则总贡献为 \(\sum_{t=1}^{m-k} (m-t+1)\binom{m-t}{k}\),应用上指标求和,令 \(s=m-t\),则原式化为 \(\sum_{s=k}^{m-1} (s+1)\binom{s}{k}\),然后把这个组合式拆开成阶乘的形式,少加整理可以得到,\(\sum_{s=k}^{m-1} (k+1)\binom{s+1}{k+1}\),把 \((k+1)\) 提出来,专门查了一下这个貌似叫冰球棍恒等式,可以把原式化为 \((k+1)\binom{m+1}{k+2}\)。第二种情况,中间随便选,这个区间都在两边,那么也就是 \([p,l-1]\)\([r+1,q]\) 任选的和再乘上中间长度选 \(k\) 个的方案数即可(因为我只钦定了 \(k\) 个所以不是 \(m!\))。最后一部分就是左端点在 \([p,l-1]\) 右端点在 \([l,r]\),(右端点在 \([r+1,q]\) 的情况对称不赘述),左端点情况数是简单的,考虑右端点,其实是和第一个一样的,枚举在 \([l,r]\) 内的长度写出来一个求和式,然后也是上指标求和可以做出来,令 \([p,l-1]\) 长度为 \(A\)\([r+1,q]\) 长度为 \(B\),则这一部分的答案为 \((A+B)\binom{m}{k+1}\)。那么钦定 \(k\)\(x\) 的方案即为:

\[T = (k+1)\binom{m+1}{k+2} + \binom{m}{k}\big(F(A)+F(B)\big) + (A+B)\binom{m}{k+1} \]

其中 \(F(t) = \dfrac{t(t+1)}{2}\)

至此,这道题的推导完成了,答案式子即为 \(R(G_x-O)+HT\),发现需要维护的只有 \(W\),用个莫队就可以维护。

代码回头再补,想了一晚上要爆炸了。。。

posted @ 2026-08-03 21:11  Cefgskol  阅读(15)  评论(0)    收藏  举报