2021ICPC区域赛(上海站)部分题解


题目链接

E - Strange Integers

签到

D - Strange Fractions

签到

G - Edge Groups

题意:

求将树边分解成两两匹配的方案数

思路:

  • 树形dp,分子树分配的边剩余是奇数还是偶数即可
  • d p i , 0 表示 i 子树边数恰好两两匹配, d p i , 1 表示多余一条边没有匹配,每个节点只有其中一个状态合法 dp_{i,0}表示i子树边数恰好两两匹配,dp_{i,1}表示多余一条边没有匹配,每个节点只有其中一个状态合法 dpi,0表示i子树边数恰好两两匹配,dpi,1表示多余一条边没有匹配,每个节点只有其中一个状态合法
  • 对于一条边 ( u , v ) ,如果 v 的状态为 d p v , 1 ,则 ( u , v ) 需要与 v 子树剩余的一条边匹配 对于一条边(u,v),如果v的状态为dp_{v,1},则(u,v)需要与v子树剩余的一条边匹配 对于一条边(u,v),如果v的状态为dpv,1,则(u,v)需要与v子树剩余的一条边匹配
  • 否则, ( u , v ) 是未匹配的边, c n t + 1 ,留给 u 子树计算贡献 否则,(u,v)是未匹配的边,cnt+1,留给u子树计算贡献 否则,(u,v)是未匹配的边,cnt+1,留给u子树计算贡献
  • 假设当前枚举到当前节点,剩余cnt条边没匹配,假设可以分成x组,则方案数为 C n 2 C n − 2 2 . . . A x x \frac{C_n^2C_{n-2}^2...}{A_x^x} AxxCn2Cn22...
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10;
const int INF=1e9;
const int mod=998244353;
int qpow(int a,int b){
    int ans=1;
    for(;b;b>>=1){
        if(b&1)ans=ans%mod*a%mod;
        a=a%mod*a%mod;
    }
    return ans;
}
int sum[N],n,fac[N],inv_fac[N];
vector<int> adj[N];
int dp[N][2];
int C(int n,int m){
    return fac[n]*inv_fac[m]%mod*inv_fac[n-m]%mod;
}
void init(){
    fac[0]=inv_fac[0]=1;
    for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod,inv_fac[i]=qpow(fac[i],mod-2);
    sum[2]=C(2,2),sum[3]=C(3,2),sum[0]=sum[1]=1;
    for(int i=4;i<=n;i++){
        sum[i]=C(i,2)*sum[i-2]%mod;
    }
    for(int i=0;i<=n;i++)dp[i][0]=dp[i][1]=-1;
}
void dfs(int u,int fa){
    if(u!=1&&adj[u].size()==1){
        dp[u][0]=1;
        return;
    }
    int cnt=0;
    int res=1;
    for(auto v:adj[u]){
        if(v==fa)continue;
        dfs(v,u);
        if(dp[v][0]!=-1)res=res*dp[v][0]%mod,cnt++;
        else res=res*dp[v][1]%mod;
    }
    // cout<<cnt<<" "<<res<<" "<<u<<"\n";
    res=res*sum[cnt]%mod*inv_fac[cnt/2]%mod;
    if(cnt&1)dp[u][1]=res;
    else dp[u][0]=res;
}
void solve(){
    cin>>n;
    init();
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    dfs(1,0);
    // for(int i=1;i<=n;i++)cout<<sum[i]<<" \n"[i==n];
    // for(int i=1;i<=n;i++)cout<<dp[i][0]<<" "<<dp[i][1]<<"\n";
    cout<<dp[1][0]<<"\n";
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    // cin>>T;
    while(T--)solve();
    return 0;
}

I - Steadily Growing Steam

思路:

带负权的背包问题,直接转移即可
d p i , j , k 表示枚举到第 i 个物品,操作了 j 次,背包容量为 k 的最大值 dp_{i,j,k}表示枚举到第i个物品,操作了j次,背包容量为k的最大值 dpi,j,k表示枚举到第i个物品,操作了j次,背包容量为k的最大值
为了防止背包容量为负,需要设置一个偏移量为p,表示背包容量为0的位置
答案: m a x i = 0 k ( d p n , i , p ) max_{i=0}^k(dp_{n,i,p}) maxi=0k(dpn,i,p)

#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10;
const int INF=1e16;
const int mod=998244353;
int dp[110][110][2610],n,k,v[110],c[110];
void solve(){
    cin>>n>>k;
    for(int i=0;i<=n;i++){
        for(int j=0;j<=k;j++){
            for(int u=0;u<=2600;u++)dp[i][j][u]=-INF;
        }
    }
    dp[0][0][1300]=0;
    for(int i=1;i<=n;i++)cin>>v[i]>>c[i];
    for(int i=1;i<=n;i++){
        for(int j=0;j<=k;j++){
            for(int u=0;u<=2600;u++){
                if(dp[i-1][j][u]==-INF)continue;
                int tmp=dp[i-1][j][u]+v[i];
                if(u+c[i]<=2600)dp[i][j][u+c[i]]=max(dp[i][j][u+c[i]],tmp);
                if(u-c[i]>=0)dp[i][j][u-c[i]]=max(dp[i][j][u-c[i]],tmp);
                if(j+1<=k&&u+2*c[i]<=2600)dp[i][j+1][u+2*c[i]]=max(dp[i][j+1][u+2*c[i]],tmp);
                if(j+1<=k&&u-2*c[i]>=0)dp[i][j+1][u-2*c[i]]=max(dp[i][j+1][u-2*c[i]],tmp);
                dp[i][j][u]=max(dp[i][j][u],dp[i-1][j][u]);
            }
        }
    }
    int ans=-INF;
    for(int i=0;i<=k;i++)ans=max(ans,dp[n][i][1300]);
    cout<<ans<<"\n";
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    // cin>>T;
    while(T--)solve();
    return 0;
}

H - Life is a Game

题意:

⼀张带边权带点权⽆向图。从某点出发,有初始声望。
每第⼀次到达⼀个点将获得点权等值的声望加成。

经过⼀条边需要满⾜边权等值的最低声望限制。

多次给出起点和初始声望,询问能达到的最⼤声望。

思路:

  • kruscal重构树
  • 按照边权从⼩到⼤建⽴Kruskal 重构树。每次询问都是从叶子出发,在树上倍增。向上找到第⼀条不能通过的边(即,该边下⾯的⼦树的叶⼦点权和加上初始声望小于该边边权),把下⾯⼦树的叶⼦点权和加上初始声望即为答案。
  • 时间复杂度: O ( ( n + m ) l o g m + q l o g n ) O((n+m)logm+qlogn) O((n+m)logm+qlogn)
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e16;
const int mod=998244353;
struct Edge{
    int u,v,w;
    bool operator<(const Edge&tmp)const{
        return w<tmp.w;
    }
};
struct DSU{
    vector<int> par;
    void init(int n){
        par.resize(n);
        iota(par.begin(),par.end(),0);
    }
    int find(int x){
        return par[x]==x?x:par[x]=find(par[x]);
    }
    bool same(int x,int y){
        return par[find(x)]==par[find(y)];
    }
    bool merge(int x,int y){
        x=find(x),y=find(y);
        if(x==y)return false;
        par[y]=x;
        return true;
    }
} dsu;
vector<Edge> e;
int n,m,q,tot,a[N],w[N],sum[N],par[N][30],cost[N][30];
void Ex_kruscal(){
    for(auto [u,v,c]:e){
        int fu=dsu.find(u),fv=dsu.find(v);
        if(fu==fv)continue;
        ++tot;
        a[tot]=a[fu]+a[fv];
        w[tot]=c;
        par[fu][0]=par[fv][0]=tot;
        cost[fu][0]=w[tot]-a[fu];
        cost[fv][0]=w[tot]-a[fv];
        dsu.par[fu]=dsu.par[fv]=tot;
    }
}
void solve(){
    cin>>n>>m>>q;
    tot=n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        e.push_back({u,v,w});
    }
    sort(e.begin(),e.end());
    dsu.init(2*n+1);
    Ex_kruscal();
    for(int j=1;j<=25;j++){
        for(int i=1;i<=tot;i++)par[i][j]=par[par[i][j-1]][j-1],cost[i][j]=max(cost[i][j-1],cost[par[i][j-1]][j-1]);
    }
    // for(int i=1;i<=tot;i++)cout<<w[i]<<" \n"[i==tot];
    while(q--){
        int x,k;
        cin>>x>>k;
        for(int i=25;i>=0;i--){
            if(cost[x][i]<=k&&par[x][i])x=par[x][i];
        }
        cout<<a[x]+k<<"\n";
    }
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    // cin>>T;
    while(T--)solve();
    return 0;
}

更容易想到的做法:离线,启发式合并

  • 对于每个节点存它的询问,然后从小到大构造最小生成树的过程中计算
  • 对于当前边的两个点,拿出点里面当前权值最小的点,判断是否能通过该条边,如果不能则计算答案,将该询问从该点移除,对于两个点都做一遍
  • 最后两边的点存储的询问都是可以通过该边的,因此启发式合并这两个点询问
  • 最后还存在的询问一定是可以遍历所有点的,计算答案
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e16;
const int mod=998244353;
struct Edge{
    int u,v,w;
    bool operator<(const Edge&tmp)const{
        return w<tmp.w;
    }
};
struct DSU{
    vector<int> par;
    void init(int n){
        par.resize(n);
        iota(par.begin(),par.end(),0);
    }
    int find(int x){
        return par[x]==x?x:par[x]=find(par[x]);
    }
    bool same(int x,int y){
        return par[find(x)]==par[find(y)];
    }
    bool merge(int x,int y){
        x=find(x),y=find(y);
        if(x==y)return false;
        par[y]=x;
        return true;
    }
} dsu;
vector<Edge> e;
int n,m,q,a[N],w[N];
set<array<int,2>> s[N];
void solve(){
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)cin>>a[i],w[i]=a[i];
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        e.push_back({u,v,w});
    }
    sort(e.begin(),e.end());
    vector<int> ans(q+1);
    for(int i=1;i<=q;i++){
        int x,k;
        cin>>x>>k;
        s[x].insert({k,i});
    }
    dsu.init(n+1);
    auto merge=[&](int u,int v){
        if(s[u].size()<s[v].size())swap(u,v);
        w[u]+=w[v];
        dsu.merge(u,v);
        for(auto it:s[v])s[u].insert(it);
        s[v].clear();
    };
    for(auto [u,v,c]:e){
        int fu=dsu.find(u),fv=dsu.find(v);
        if(fu==fv)continue;
        while(s[fu].size()&&(*s[fu].begin())[0]+w[fu]<c){
            auto [x,id]=*s[fu].begin();
            s[fu].erase(s[fu].begin());
            ans[id]=x+w[fu];
        }
        while(s[fv].size()&&(*s[fv].begin())[0]+w[fv]<c){
            auto [x,id]=*s[fv].begin();
            s[fv].erase(s[fv].begin());
            ans[id]=x+w[fv];
        }
        merge(fu,fv);
    }
    for(int i=1;i<=n;i++){
        while(s[i].size()){
            auto [x,id]=*s[i].begin();
            s[i].erase(s[i].begin());
            ans[id]=x+w[i];
        }
    }
    for(int i=1;i<=q;i++)cout<<ans[i]<<"\n";
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    // cin>>T;
    while(T--)solve();
    return 0;
}

M - Harmony in Harmony

题意:

将地分成n部分,第二次忘了前一次的分配,又将地分成n部分,求每个人两次分配的土地面积的交集最小值最大

思路:

神奇的构造题

  • 此题等价于存在⼀个两侧各有n个结点的带边权的满⼆分图,边权为实数,并且对任何⼀个结点,其所连边的权值和等于1/n。要求寻找⼀个尽可能⼤的 ans,使得在任何满⾜上述条件的⼆分图下,都能够找到⼆分图的完美匹配,使得匹配边的权值都不低于ans
  • 取左边 t − 1 个黑点和右边 t 个白点,假设,左边每个黑点对右边的每个白点的贡献 1 n t 取左边t-1个黑点和右边t个白点,假设,左边每个黑点对右边的每个白点的贡献\frac{1}{nt} 取左边t1个黑点和右边t个白点,假设,左边每个黑点对右边的每个白点的贡献nt1
  • 则每个白点的贡献还剩余 1 n − t − 1 n t = 1 n t 则每个白点的贡献还剩余\frac{1}{n}-\frac{t-1}{nt}=\frac{1}{nt} 则每个白点的贡献还剩余n1ntt1=nt1
  • 将贡献平分给 n − t + 1 个黑点,每个黑点得到的贡献为 1 n t ( n − t + 1 ) 将贡献平分给n-t+1个黑点,每个黑点得到的贡献为\frac{1}{nt(n-t+1)} 将贡献平分给nt+1个黑点,每个黑点得到的贡献为nt(nt+1)1
  • t 取 ⌊ n + 1 2 ⌋ 取最小,答案为 1 n ⌊ n + 1 2 ⌋ ⌊ n + 1 2 ⌋ t取\lfloor \frac{n+1}{2} \rfloor取最小,答案为\frac{1}{n \lfloor \frac{n+1}{2} \rfloor \lfloor \frac{n+1}{2} \rfloor} t2n+1取最小,答案为n2n+12n+11
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e9;
const int mod=998244353;
void solve(){
    int n;
    cin>>n;
    double ans=1.0/(1.0*n*((n+1)/2)*((n+2)/2));
    cout<<setprecision(9)<<fixed<<ans<<"\n";
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    // cin>>T;
    while(T--)solve();
    return 0;
}

K - Circle of Life

构造题

  • 注意到长度为2时,有"10"
  • 长度为4时,有"1001"
  • 长度为5时,有"10001"
  • 且4和5可以拼接
  • 除了3都可以构造出来
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e9;
const int mod=998244353;
void solve(){
    int n;
    cin>>n;
    if(n==3)cout<<"Unlucky\n";
    else{
        int x=0,y=0,z=0;
        if(n%2)z=1,n-=5;
        y=n/4;
        if(n%4!=0)x=1;
        for(int i=1;i<=z;i++)cout<<"10001";
        for(int i=1;i<=y;i++)cout<<"1001";
        for(int i=1;i<=x;i++)cout<<"10";
    }
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    // cin>>T;
    while(T--)solve();
    return 0;
}

J - Two Binary Strings 问题

题意:

给出 01 序列, A , B 给出01序列,A,B 给出01序列,AB
定义 f ( l , r ) ,如果 1 的个数大于等于长度一半,则为 1 ,否则为 0 定义f(l,r),如果1的个数大于等于长度一半,则为1,否则为0 定义f(l,r),如果1的个数大于等于长度一半,则为1,否则为0
对于一个 k ,如果所有 i 满足 f ( m a x ( 1 , i − k + 1 ) , i ) = B i 则称 k 合法 对于一个k,如果所有i满足f(max(1,i-k+1),i)=B_i则称k合法 对于一个k,如果所有i满足f(max(1,ik+1),i)=Bi则称k合法
输出一个长度为 n 的二进制,为 1 表示第 k 位合法 输出一个长度为n的二进制,为1表示第k位合法 输出一个长度为n的二进制,为1表示第k位合法

思路:

  • 转化为求前缀和,如果 s i − k ≤ s i 则 k 合法 转化为求前缀和,如果s_{i-k} \leq s_i 则k合法 转化为求前缀和,如果siksik合法
  • 对于每个 i 求出满足的前缀即可 对于每个i求出满足的前缀即可 对于每个i求出满足的前缀即可
  • 可以用bitset优化暴力
  • 使用一个val[i]存储前缀和值为i的下标集合,a集合维护当前大于sum[i]的下标集合,b集合维护小于等于的
  • 注意到每次sum[i]值只会增加一或减少一,根据此信息更新i下标在哪或者i-1下标在哪个集合即可
#include<bits/stdc++.h>
using namespace std;
const int N=50010;
bitset<N> val[N],a,b,ans,all;
void solve(){
    all.set();
    int n;
    cin>>n;
    string s,t;
    cin>>s>>t;
    s=" "+s,t=" "+t;
    vector<int> sum(n+1);
    a.reset(),b.reset(),ans.set();
    for(int i=0;i<=n;i++)val[i].reset();
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+(s[i]=='1'?1:-1);
    map<int,int> mp;
    int idx=0;
    auto id=[&](int x){
        if(mp.count(x))return mp[x];
        return mp[x]=++idx;
    };
    val[id(0)][0]=1;
    b[0]=1;
    for(int i=1;i<=n;i++){
        if(sum[i]>sum[i-1])a^=val[id(sum[i-1])],b^=val[id(sum[i-1])];
        else a^=val[id(sum[i])],b^=val[id(sum[i])];
        if(t[i]=='1'){
            bitset<N> tmp=a<<(n-i+1);
            if(a[0])tmp|=(all>>(N-n+i-1));
            ans&=tmp;
        }
        else{
            bitset<N> tmp=b<<(n-i+1);
            if(b[0])tmp|=(all>>(N-n+i-1));
            ans&=tmp;
        }
        val[id(sum[i])][i]=1;
        b[i]=1;
    }
    for(int i=n;i>=1;i--)cout<<ans[i];
    cout<<"\n";
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    int T=1;
    cin>>T;
    while(T--)solve();
    return 0;
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值