【学习笔记】AGC062

文章探讨了在处理数据时如何考虑元素相对位置关系,通过状态转移和二进制策略解决复杂问题。提到在某些情况下,可以放宽限制以简化问题,并详细分析了不同操作对序列和前缀的影响。此外,还讨论了暴力维护在特定条件下的有效性,以及在处理绝对值问题时的几何和代数方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

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

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

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

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

复杂度O(n4)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];
}

难啊。还得是你AGCAGCAGC的题目。。。

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

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

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

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

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

复杂度O(N2Klog⁡(NK))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)能走到的点构成了一个矩形边框。同时可以通过二分的手段将棋子能走的范围限制在一个矩形边框内,那么一个棋子下一步合法当且仅当两个矩形边框有交,也就是说DiD_iDi不能超过(x,y)(x,y)(x,y)到矩形边界四个角的最大距离,这样第一步就很好的转化出来了。更关键一步的转化是,如果当前点在二分的矩形边界上,并且Di≤2RD_i\le 2RDi2R(其中RRR是二分的答案),那么两个矩形边框总能相交,也就是说这个点总能留在二分的矩形边框上,RRR就一定合法。这同样可以通过严格的代数分析来证明。

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

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

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

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

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

1.11.11.1 如果x,yx,yx,y都存在,那么等价于将<R<R<RDiD_iDi划分成两个集合,一部分≥x−R\ge x-RxR,另一部分≥y−R\ge y-RyR

1.21.21.2 如果yyy不存在,那么等价于将<R<R<RDiD_iDi划分成两个集合,一部分≥x−R\ge x-RxR,另一部分≥R\ge RR。不难发现就是y=2Ry=2Ry=2R的情况。

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

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

复杂度O(n2w)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;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值