【学习笔记】AGC062

这场的数据范围非常微妙。

注意到初始局面是固定的,这提示我们倒着考虑问题。

考虑两个元素的相对位置关系。称一段前缀被还原了,当且仅当前缀中任意两个数的相对位置关系是保证的。这样每次操作事实上是选 K K K个元素出来然后拼接到末尾。可以看出的是,被还原的前缀只会增多不会减少,因此可以直接记入状态。进一步转化,一段前缀 [ 1 , l ] [1,l] [1,l]被还原当且仅当对于任意 i ∈ [ 1 , l ) i\in [1,l) i[1,l) i i i i + 1 i+1 i+1的相对位置关系得到保证。

至此,我们可以清晰的看清楚其中的约束关系:被挪到末尾的元素的集合是确定的,每次挪动时 i i i i + 1 i+1 i+1不能同时被挪到末尾。但是这个限制太难处理了,我尝试对连续段贪心但是未果。

考虑将限制放宽,这样我们可以避开贪心直接求解:构造序列 { X i } \{X_i\} {Xi},如果第 k k k次操作将 i i i挪到了末尾,那么 X i ← X i + 2 k X_i\gets X_i+2^k XiXi+2k。记 { Q i } \{Q_i\} {Qi} { P i } \{P_i\} {Pi}的逆排列,据上面分析我们可以写出 { X i } \{X_i\} {Xi}合法的充要条件:对于任意 i ∈ [ 1 , n ) i\in [1,n) i[1,n),如果 Q i < Q i + 1 Q_i<Q_{i+1} Qi<Qi+1那么 X i ≤ X i + 1 X_i\le X_{i+1} XiXi+1,如果 Q i > Q i + 1 Q_i>Q_{i+1} Qi>Qi+1那么 X i < X i + 1 X_i<X_{i+1} Xi<Xi+1。这样我们巧妙地把序列问题和二进制勾连起来了。

二进制我熟啊。从高到低位考虑,因为涉及到连续段所以要套一个区间 d p dp dp所以还是要回到区间啊。

复杂度 O ( n 4 ) O(n^4) O(n4)

#include<bits/stdc++.h>
#define db double
#define fi first
#define se second
#define ll long long
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
int n,K,p[105],pre[105];
ll c[105],dp[105][105][105];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>K;
    for(int i=1;i<=K;i++)cin>>c[i];
    for(int i=1;i<=n;i++){
        int x;cin>>x,p[x]=i;
    }
    for(int i=2;i<=n;i++)pre[i]=pre[i-1]+(p[i-1]>p[i]);
    memset(dp,0x3f,sizeof dp);
    for(int i=1;i<=n;i++){
        for(int j=i;j<=n;j++){
            if(pre[j]-pre[i]==0){
                dp[K+1][i][j]=0;
            }
        }
    }
    for(int i=K;i>=1;i--){
        for(int j=1;j<=n;j++){
            for(int k=j;k<=n;k++){
                dp[i][j][k]=dp[i+1][j][k];
                for(int l=j;l<k;l++){
                    dp[i][j][k]=min(dp[i][j][k],dp[i+1][j][l]+dp[i+1][l+1][k]+c[i]*(k-l));
                }
            }
        }
    }
    if(dp[1][1][n]==inf)cout<<-1;
    else cout<<dp[1][1][n];
}

难啊。还得是你 A G C AGC AGC的题目。。。

看到 N , K N,K N,K这么小,考虑直接暴力维护。加入一个元素的本质就是将原集合 S S S右移 a i a_i ai位然后取并。经验告诉我们 a i a_i ai应该按从小到大加入,这样连续段的数目最小。但是如果分析不仔细的话可能就直接上暴力维护了,显然 K K K这么小就没用上。

很难相信, A G C AGC AGC题目的正解竟然是暴力讨论。记 S i = ∑ j ≤ i A i S_i=\sum_{j\le i}A_i Si=jiAi X i X_i Xi表示 1 ∼ S i 1\sim S_i 1Si中不能被表示为子集和的数的个数,经验告诉我们可以按从小到大的顺序加数,不妨来细致讨论一下。问题来了,为啥能想到这题直接讨论就行?

1.1 1.1 1.1 A i > S i − 1 A_i>S_{i-1} Ai>Si1,那么 ( X i − 1 ∪ [ S i − 1 + 1 , A i − 1 ] ) ⊆ X i (X_{i-1}\cup [S_{i-1}+1,A_{i}-1])\subseteq X_i (Xi1[Si1+1,Ai1])Xi,并且对于 v ∈ [ A i , S i ] v\in [A_i,S_i] v[Ai,Si] v ∈ X i ⇔ v − A i ∈ X i − 1 v\in X_i\Leftrightarrow v-A_i\in X_{i-1} vXivAiXi1,这样的检查是容易的,不妨先假设后面的数对前面没有影响。或者直接看成是区间平移,假装有一个数据结构来维护它。

1.2 1.2 1.2 A i ≤ S i − 1 A_i\le S_{i-1} AiSi1,那么我们先对 [ 1 , A i − 1 ] [1,A_i-1] [1,Ai1]中的数进行检查,如果超过 K K K就可以直接结束了。套用前面的观察(区间平移),我们意识到可以将 ∣ X i ∣ |X_i| Xi控制在一定范围内。仍然可以将一个元素是否在 X i X_i Xi中的充要条件写出:对于 v ∈ [ A i , S i − 1 ] v\in [A_i,S_{i-1}] v[Ai,Si1] v ∈ X i ⇔ v − A i ∈ X i − 1 v\in X_i\Leftrightarrow v-A_i\in X_{i-1} vXivAiXi1 v ∈ X i − 1 v\in X_{i-1} vXi1,对于 v ∈ [ S i − 1 + 1 , S i ] v\in [S_{i-1}+1,S_i] v[Si1+1,Si] v ∈ X i ⇔ v − A i ∈ X i − 1 v\in X_i\Leftrightarrow v-A_i\in X_{i-1} vXivAiXi1。显然,我们可以通过遍历 X i − 1 X_{i-1} Xi1中的元素从而将 X i X_i Xi求出。

那么问题来了, ∣ X i ∣ |X_i| Xi到底有多大?问题的症结出现在寻找的过程中。注意到第二种情况中,当 v ∈ [ A i , S i − 1 ] v\in [A_i,S_{i-1}] v[Ai,Si1]时,这段区间里有值的数不会超过 K K K个。当 v ∈ [ S i − 1 + 1 , S i ] v\in [S_{i-1}+1,S_i] v[Si1+1,Si]时有值的数不会超过 ∣ X i − 1 ∣ |X_{i-1}| Xi1个,于是 ∣ X i ∣ |X_i| Xi大小呈线性增长,换句话说任意时刻不超过 2 N K 2NK 2NK。这已经足够快了。

复杂度 O ( N 2 K log ⁡ ( N K ) ) O(N^2K\log(NK)) O(N2Klog(NK))。实际肯定跑的还要快一些。

其实认真分析后发现并不困难,但是当时可能还是把这道题想的太结论了。

#include<bits/stdc++.h>
#define db double
#define fi first
#define se second
#define ll long long
#define pb push_back
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
int n,K;
ll a[65],s[65];
set<ll>X;
void getans(){
    assert(X.size()>=K);
    for(auto v:X){
        cout<<v<<" ";
        if(--K==0)exit(0);
    }
    assert(0);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>K;for(int i=1;i<=n;i++)cin>>a[i];
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
    for(int i=1;i<=n;i++){
        if(a[i]>s[i-1]){
            if(X.size()>=K){
                getans();
            }
            for(ll j=s[i-1]+1;j<a[i];j++){
                X.insert(j);
                if(X.size()>=K){
                    getans();
                }
            }
            vector<ll>vec;
            for(auto v:X){
                if(v<=s[i-1]){
                    vec.pb(v+a[i]);
                }
            }
            for(auto v:vec)X.insert(v);
        }
        else{
            vector<ll>vec,vec2;
            for(auto v:X){
                if(v<a[i]){
                    vec.pb(v);
                    if(vec.size()==K){
                        for(auto x:vec){
                            cout<<x<<" ";
                        }
                        return 0;
                    }
                }
                if(v+a[i]>s[i-1]||X.count(v+a[i])){
                    vec2.pb(v+a[i]);
                }
            }
            X.clear();
            for(auto v:vec)X.insert(v);
            for(auto v:vec2)X.insert(v);
        }
        assert(X.size()<=n*K);
    }
    if(X.size()<K){
        for(ll i=s[n]+1;;i++){
            X.insert(i);
            if(X.size()==K)break;
        }
    }
    getans();
}

这题是真难。

还是从拆分绝对值入手。可以用代数的手段发现棋子从 ( x , y ) (x,y) (x,y)能走到的点构成了一个矩形边框。同时可以通过二分的手段将棋子能走的范围限制在一个矩形边框内,那么一个棋子下一步合法当且仅当两个矩形边框有交,也就是说 D i D_i Di不能超过 ( x , y ) (x,y) (x,y)到矩形边界四个角的最大距离,这样第一步就很好的转化出来了。更关键一步的转化是,如果当前点在二分的矩形边界上,并且 D i ≤ 2 R D_i\le 2R Di2R(其中 R R R是二分的答案),那么两个矩形边框总能相交,也就是说这个点总能留在二分的矩形边框上, R R R就一定合法。这同样可以通过严格的代数分析来证明。

那么问题就转化为了从 ( 0 , 0 ) (0,0) (0,0)出发能否到达二分的矩形边界上。可以证明只朝一个方向移动是最优的。

但是题目要求还要回到 ( 0 , 0 ) (0,0) (0,0),这怎么处理呢?问题的症结在于我们的分析不够细致。考虑将 D i D_i Di从小到大排序,可以证明 R R R合法应该满足 D n 2 ≤ R ≤ D n \frac{D_n}{2}\le R\le D_n 2DnRDn,下界很好理解,前面已经分析过了;这个上界很有意思,考虑当 R = D n R=D_n R=Dn时可以用除 D n D_n Dn外所有步数走到矩形边界上然后留住,最后恰好走 D n D_n Dn步回到原点,这样就证完了。这个地方还是比较难想到的。当然顺便可以发现有解的充要条件是 ∑ i < n D i ≥ D n \sum_{i<n}D_i\ge D_n i<nDiDn,这是容易判断的,因此不再赘述。

有一个非常不符合直观的结论:如果点集 { D i } \{D_i\} {Di}能走到边界上的任意一个点,那么一定能走到所有的矩形边界。为什么?这个时候直观法就变得非常难受了。不妨这样来想:将每个 D i D_i Di想象成一个向量 ( x i , y i ) (x_i,y_i) (xi,yi),满足 ∣ x i ∣ + ∣ y i ∣ = D i |x_i|+|y_i|=D_i xi+yi=Di,考虑从第一象限的 ( X , Y ) (X,Y) (X,Y)调整到 ( X − 1 , Y + 1 ) (X-1,Y+1) (X1,Y+1),只需要存在一个向量满足 x i y i > 0 x_iy_i>0 xiyi>0,否则说明只存在朝向二四象限的向量,将这些向量进行调整直到出现朝向 x x x轴正方向的向量,这个时候就可以走到 ( X − 1 , Y + 1 ) (X-1,Y+1) (X1,Y+1)了。又因为坐标系是可以旋转的所以能够将所有矩形边界遍历到。这样就把这个非常关键的结论证明到了。

可以证明,最优方案一定经过了矩形边界,这样问题就等价于将 { D i } \{D_i\} {Di}分成两个集合,使得两个集合都能到达矩形边界。这就回到我们之前的观察了,只考虑朝一个方向走的情况,设所有 < R <R <R D i D_i Di的和为 S S S,如果 S ≥ R S\ge R SR那么合法;否则考虑第一个 ≥ R \ge R R的数 D i D_i Di,如果 D i ≤ S + R D_i\le S+R DiS+R那么合法,否则不合法。这个判定的过程要想清楚。比如我就以为只要判集合中最大的数就好了,实际上不是这样的。

因为我们假设了 R R R是固定的,所以根据上面的分析我们发现只要保留 ≥ R \ge R R D i D_i Di当中最小和次小的数 x , y x,y x,y即可。分两种情况讨论:

1.1 1.1 1.1 如果 x , y x,y x,y都存在,那么等价于将 < R <R <R D i D_i Di划分成两个集合,一部分 ≥ x − R \ge x-R xR,另一部分 ≥ y − R \ge y-R yR

1.2 1.2 1.2 如果 y y y不存在,那么等价于将 < R <R <R D i D_i Di划分成两个集合,一部分 ≥ x − R \ge x-R xR,另一部分 ≥ R \ge R R。不难发现就是 y = 2 R y=2R y=2R的情况。

可以用 bitset \text{bitset} bitset这一工具帮助我们计算。

R R R从小到大枚举并同时添加物品就可以完成计算了。这样也可以规避掉二分正确性的问题。

复杂度 O ( n 2 w ) O(\frac{n^2}{w}) O(wn2)

还是太菜了,基本上所有结论都是看的题解。。。

感觉也没用到什么高深的知识点,但是就是理解了很久啊。。。

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
int n;
ll S;
ll d[200005];
bitset<400005>b;
signed main(){
    //freopen("data.in","r",stdin);
    // freopen("own.out","w",stdout);
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;for(int i=1;i<=n;i++)cin>>d[i];
    sort(d+1,d+1+n);
    for(int i=1;i<n;i++)S+=d[i];
    if(S<d[n]){
        cout<< -1;
        return 0;
    }
    S=0;
    int j=1;b[0]=1;
    for(int i=d[n]/2;i<=d[n];i++){
        while(d[j]<i){
            b|=(b<<d[j]);
            S+=d[j++];
        }
        int x=d[j]-i,y=(j!=n)?d[j+1]-i:i;
        int k=b._Find_next(x-1);
        if(k!=b.size()&&S-k>=y){
            cout<<i;
            return 0;
        }
    }
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值