3.21 ZYZ(aoao Round 4)赛后题解

counting is fun Round。

四道 DP。有点神秘。

这场都是好题啊。upvote 一下。

A.counting is fun 1

P2734 [IOI 1996 / USACO3.3] 游戏 A Game

有点经典的 trick。

Description

有一个双端队列,小 \(\delta\) 和小 \(\mu\) 每次可以分别从某一端取出一个数(小 \(\delta\) 先手),得分是一个人取出的数的和,现在两人都想最大化自己的得分,问你如果两人都采取最优策略,那么两人得分的最大值分别是多少。

Solution

因为是双端队列,所以一个人面对的状态一定是原序列的一个区间。

所以设两个 \(dp\) 数组分别表示小 \(\delta\) 和小 \(\mu\) 分别面对区间 \([l,r]\) 时能取到的最大值是多少。

转移的话两个数组相互转就行了。

然后你算一下空间发现这个东西开不下两个 \(dp\) 数组,但你注意到这个带 \(\dfrac{1}{2}\) 常数,所以你用 vector 压一下就行了。

复杂度 \(O(n^2)\)

Code

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fll
using namespace std;

int n;
int a[5010],pre[5010];

vector<vector<int> > dp1,dp2;

int main(){
    cin.tie(0)->sync_with_stdio(false);
    
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) pre[i]=pre[i-1]+a[i];

    dp1.resize(n+1);
    for(int i=1;i<=n;i++) dp1[i].resize(n-i+1);

    dp2.resize(n+1);
    for(int i=1;i<=n;i++) dp2[i].resize(n-i+1);

    for(int i=1;i<=n;i++) dp1[i][0]=dp2[i][0]=a[i];
    for(int len=2;len<=n;len++) for(int l=1,r=len;r<=n;l++,r++){
        dp2[l][r-l]=max((pre[r]-pre[l]-dp1[l+1][r-(l+1)])+a[l],(pre[r-1]-pre[l-1]-dp1[l][r-1-l])+a[r]);
        dp1[l][r-l]=max((pre[r]-pre[l]-dp2[l+1][r-(l+1)])+a[l],(pre[r-1]-pre[l-1]-dp2[l][r-1-l])+a[r]);
    }
    cout<<dp1[1][n-1]<<" "<<pre[n]-dp1[1][n-1]<<"\n";
    
    # ifdef TakanashiHoshino
    cerr<<"\nUsed time: "<<clock()*1.0/CLOCKS_PER_SEC<<"s.\n";
    # endif
    return 0;
}

B.counting is fun 2

P7335 [JRKSJ R1] 异或

什么叫赛时代码抢到了最优解第一。

Description

你有一个序列,你要从中选出 \(k\) 个不交区间 \([l_i,r_i]\),使得 \(\sum\limits_{i=1}^{k}\operatorname{xor}_{j=l_i}^{r_i}a_j\) 最大化。

保证序列随机生成。

Solution

题解

考虑 DP。

\(dp_{p,i}\) 表示考虑前 \(i\) 个位置分了 \(p\) 个区间的答案。

因为发现每次转移都只需要上一层,所以可以把 DP 数组压成一维的,重复做 \(k\) 次即可。

朴素做法是对每个 \(i\) 枚举所有可能的 \(j\),转移 \(dp_{i}\gets\max(dp_{i},dplast_{j-1}+\operatorname{xor}_{l=j}^{i}a_l)\)

最后对 DP 数组做一遍前缀 \(\max\)


现在考虑数据随机带来的性质。

考虑一个点往左,哪些决策点是可能的。

从这个点往左,做一个后缀异或和,则所有可能的决策点是后缀异或和的每个后缀最大值的位置。因为非后缀最大值位置会被其右边的后缀最大值位置偏序。

对每个 \(i\) 而言这个决策点的数量期望应该是 \(O(\log V)\) 的。这等价于随机序列前缀 \(\max\) 的数的数量。

处理出来这个信息,每次暴力转移,跑 \(k\) 次 DP,就做完了。

期望复杂度应该是 \(O(nk\log V)\)

Code

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fll
using namespace std;

int n,k;
int a[3010],pre[3010];

vector<int> ts[3010];

long long dp[3010],dp_last[3010];

void solve(){
    memcpy(dp_last,dp,(n+1)*(sizeof(long long)));
    memset(dp,0,(n+1)*(sizeof(long long)));
    for(int i=1;i<=n;i++) for(int x:ts[i]) dp[i]=max(dp[i],dp_last[x-1]+(pre[i]^pre[x-1]));
    for(int i=1;i<=n;i++) dp[i]=max(dp[i],dp[i-1]);
}

int main(){
    cin.tie(0)->sync_with_stdio(false);
    
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) pre[i]=pre[i-1]^a[i];

    for(int i=1;i<=n;i++){
        int nowxor=0,maxxor=-1;
        for(int p=i;p>=1;p--){
            nowxor^=a[p];
            if(nowxor>maxxor) ts[i].push_back(p),maxxor=nowxor;
        }
    }

    while(k--) solve();
    cout<<dp[n]<<"\n";
    
    # ifdef TakanashiHoshino
    cerr<<"\nUsed time: "<<clock()*1.0/CLOCKS_PER_SEC<<"s.\n";
    # endif
    return 0;
}

C.counting is fun 3

P10547 [THUPC 2024 决赛] 排列游戏

两个条件单独拿出来计数我分别都会但是为什么合一起我就不会了。

非常 edu 的纯 counting。

Description

你有一个排列 \(p\),初始 \(p_i=i\)

\(\delta\) 会进行恰好 \(n\) 次操作,每次交换两个不同位置上的 \(p\)

\(\mu\) 可以花费 \(|i-j|\) 的时间交换 \(p_i,p_j\),现在问你,有多少种小 \(\delta\) 操作后得到的排列,使得小 \(\mu\) 能用至多 \(m\) 时间还原回初始状态。

答案对 \(10^9+7\) 取模。

Solution

看不懂的可以去看这个写的不错的题解。

刻画两个条件。

花费 \(|i-j|\) 的时间交换 \(p_i,p_j\),变回初始排列的最少时间是 \(\frac{1}{2} \sum_{i=1}^{n}|i-p_i|\)

把每项拆开到每一段 \([k,k+1]\) 上,这是经典转化。

\(n\) 次交换两个不同位置上的 \(p\),考虑排列的置换环,每次交换显然会改变置换环的奇偶性,\(n\) 次操作后显然置换环数为偶数。

把排列看成 \(n\)\(i\to p_i\) 的有向边,按从小到大的顺序依次加入每个 \(p_w\)

\(dp_{i,j,k,0/1}\) 表示考虑到了 \(p_w=i\) 的边,目前有 \(j\) 条链,对第一个限制的贡献为 \(k\),置换环数量奇偶性 \(0/1\)

转移分别枚举这条边对前面所有链结合的情况:新加链、自环、接到已有链的两侧、合并两条链、将一条链合成环。

\(k'=k+j\),即限制一中的贡献形式。

  • 新加链,\(dp_{i,j+1,k',x} \gets dp_{i,j+1,k',x}+dp_{i-1,j,k,x}\)
  • 自环,\(dp_{i,j,k',1-x} \gets dp_{i,j,k',1-x}+dp_{i-1,j,k,x}\)
  • 接到已有链的两侧,\(dp_{i,j,k',x} \gets dp_{i,j,k',x}+2\times j \times dp_{i-1,j,k,x}\)
  • 合并两条链,\(dp_{i,j-1,k',x} \gets dp_{i,j-1,k',x}+j \times (j-1) \times dp_{i-1,j,k,x}\)
  • 将一条链合成环,\(dp_{i,j-1,k',1-x} \gets dp_{i,j-1,k',1-x}+j \times dp_{i-1,j,k,x}\)

初始化 \(dp_{0,0,0,0}=1\),答案即为 \(\sum_{k=0}^m dp_{n,0,k,0}\)

第二维每次会增加 \(j\),上界开到 \(O(\sqrt m)\) 级别就够了。这也是个常见优化。

需要滚动数组和离线询问。

复杂度 \(O(nm\sqrt m)\)

Code

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fll
using namespace std;

static const unsigned long long mod=1e9+7;
class modint{
    // ...
};

int q;
vector<pair<int,int> > qr[510];

int n,m;
modint dp[2][80][5010][2];

const int S=75;
modint Ans[1010];

int main(){
    cin.tie(0)->sync_with_stdio(false);
    
    cin>>q;
    for(int i=1;i<=q;i++){
        cin>>n>>m;
        qr[n].push_back({m,i});
    }

    n=500,m=5000;
    dp[0][0][0][0]=1;

    for(int wi=1,i=1;wi<=n;wi++,i^=1){
        memset(dp[i],0,sizeof dp[i]);
        for(int j=0;j<=min(wi,S);j++){
            for(int k=0,nk=j;nk<=m;k++,nk++){
                for(int x:{0,1}){
                    modint lv=dp[i^1][j][k][x];
                    if(lv==0) continue;

                    if(j+1<=S) dp[i][j+1][nk][x]+=lv;
                    dp[i][j][nk][x^1]+=lv;
                    dp[i][j][nk][x]+=lv*2*j;
                    if(j) dp[i][j-1][nk][x^1]+=lv*j;
                    if(j) dp[i][j-1][nk][x]+=lv*j*(j-1);
                }
            }
        }
        for(auto [qm,id]:qr[wi]) for(int k=0;k<=qm;k++) Ans[id]+=dp[i][0][k][0];
    }

    for(int i=1;i<=q;i++) cout<<Ans[i]<<"\n";

    # ifdef TakanashiHoshino
    cerr<<"\nUsed time: "<<clock()*1.0/CLOCKS_PER_SEC<<"s.\n";
    # endif
    return 0;
}

D.counting is fun 4

P9047 [PA 2021] Poborcy podatkowi

好题。

Description

你有一棵树,你可以从树上选出若干条边不相交的长度为 \(4\) 的链(指边数),选的权值是这些链的并集中的所有边的边权和。

你需要最大化选的权值。

Solution

不难想到树形 DP。

设状态 \(dp_{x,0/1/2/3}\) 表示以 \(x\) 为根的子树,从 \(x\) 处向上的链长为 \(0/1/2/3\) 时的最大答案。

转移考虑一个点的所有子树,需要对这些子树两两配对:\(0\)\(2\) 配对,\(1\)\(1\) 配对,\(3\) 单独贡献进答案,最后挑一个子树链,加上一条边分别尝试贡献给四个状态。

这类似于一个分组背包,一个儿子只能选择一个状态进行贡献。

但是这个背包比较有性质,物品的重量为 \(1\)\(dp_{y,0}\))、\(0\)\(dp_{y,1/3}\))、\(-1\)\(dp_{y,2}\))中的一个。

考虑临时背包数组 \(f_{i,0/1}\) 表示当前背包选择的重量之和为 \(i\),即选择的 \(dp_{y,0}\) 数量和选择的 \(dp_{y,2}\) 数量之差,且选择了奇数或偶数个 \(0\)\(dp_{y,1}\))时最大的答案。

最终有:

\[\begin{cases} dp_{x,0}=f_{0,0}\\ dp_{x,1}=f_{1,0}\\ dp_{x,2}=f_{0,1}\\ dp_{x,3}=f_{-1,0} \end{cases} \]

但是这样背包计算是 \(O(n)\) 的。

考虑最终合法的方案的形态,决策形如一个全部由 \(-1\)\(0\)\(1\) 组成的序列。

最坏情况下,这些决策对应的重量之和会达到 \(O(n)\) 级别。

但是我们有一个经典结论:对于一个长为 \(n\),元素只有 \(-1\)\(1\) 且所有元素和为 \(0\) 或接近 \(0\) 的序列,将其随机打乱,前缀和的绝对值的最大值是 \(O(\sqrt n)\) 量级的。

\(-1\)\(0\)\(1\) 组成的序列对应的结论显然不会弱于上述结论。

所以我们在转移之前把所有儿子随机打乱,背包容量的上界开到 \(O(\sqrt n)\) 就够了。

Code

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fll
using namespace std;

mt19937 mtg((unsigned int)chrono::high_resolution_clock().now().time_since_epoch().count());
long long f(long long l,long long r){return mtg()%(r-l+1)+l;}

struct Graph{
    struct edge{int to,nxt;long long w;}e[400010];
    int head[200010],ecnt=1;

    inline void addedge(int x,int y,long long w){e[ecnt]={y,head[x],w},head[x]=ecnt++;}
    vector<pair<int,long long> > operator[](int x){
        vector<pair<int,long long> > ans;
        for(int i=head[x];i;i=e[i].nxt) ans.push_back({e[i].to,e[i].w});
        return ans;
    }
}g;

int n;
long long dp[200010][4];

const int P=471;
long long tmp[470<<1|1][2],tmp2[470<<1|1][2];

void dfs(int x,int p=0){
    vector<pair<int,long long> > v=g[x];
    for(auto [y,w]:v) if(y!=p) dfs(y,x);

    shuffle(v.begin(),v.end(),mtg);

    memset(tmp,-0x3f,sizeof tmp);
    tmp[P][0]=0;

    for(auto [y,w]:v) if(y!=p){
        memset(tmp2,-0x3f,sizeof tmp2);
        for(int i=-450;i<=450;i++){
            for(int z:{0,1}){
                tmp2[i+P][z]=max(tmp2[i+P][z],tmp[i+P][z]+dp[y][0]);
                if(i+1<=450) tmp2[i+1+P][z]=max(tmp2[i+1+P][z],tmp[i+P][z]+dp[y][0]+w);
                tmp2[i+P][z^1]=max(tmp2[i+P][z^1],tmp[i+P][z]+dp[y][1]+w);
                if(i-1>=-450) tmp2[i-1+P][z]=max(tmp2[i-1+P][z],tmp[i+P][z]+dp[y][2]+w);
                tmp2[i+P][z]=max(tmp2[i+P][z],tmp[i+P][z]+dp[y][3]+w);
            }
        }
        memcpy(tmp,tmp2,sizeof tmp);
    }
    dp[x][0]=max(0ll,tmp[P][0]);
    dp[x][1]=tmp[P+1][0];
    dp[x][2]=tmp[P][1];
    dp[x][3]=tmp[P-1][0];
}

int main(){
    cin.tie(0)->sync_with_stdio(false);
    
    cin>>n;
    for(int i=1;i<n;i++){
        int x,y,w;cin>>x>>y>>w;
        g.addedge(x,y,w),g.addedge(y,x,w);
    }
    dfs(1);

    cout<<dp[1][0]<<"\n";
    
    # ifdef TakanashiHoshino
    cerr<<"\nUsed time: "<<clock()*1.0/CLOCKS_PER_SEC<<"s.\n";
    # endif
    return 0;
}
posted @ 2026-03-24 13:43  AeeE5x  阅读(3)  评论(0)    收藏  举报