NOIP 提高组模拟赛5

math

emmmmmm,这题我跑了个奇奇怪怪的背包,然后用奇奇怪怪的优化,然后奇奇怪怪的AC了

upd: 做法假了,随便拍点随机数据就能卡掉.... (2022.7.11),新加了几个数据,不能AC了。....

先说我奇奇怪怪的想法,发现本题式子可以理解为每个数可以取任意个,能够凑出的数有哪些,貌似可以完全背包

然鹅,因为存在模k这个操作,直接跑完全背包显然是错的,跑多些容量应该是可行的,但是跑多少是难以预估的,(MLETLE警告),然后我们考虑最多取多少个物品,之后再取都没有意义,显然,让物品大小j不停的加a[i]k,等到再次等于a[i]即可停止

然鹅,这样会喜提TLE,考虑继续优化,发现其实我们会多次取到同一个容量,造成了许多无意义的判断,如何简化?首先对原数组取模后排序,然后每次判断一下1单位的新的容量是否已经存在,如果存在,那说明有步长比当前容量小的已经更新过了,那么当前容量没有必要更新,这样能大大降低复杂度。最终复杂度O()

再说一下题解做法,其他大佬也都是这个思路,以下内容照抄题解

ax+by=z存在整数解,当且仅当gcd(a,b)z
那么,若z可以被凑出,即 i=1nxiai=z,当且仅当gcd(a1,a2,,an)z。因此,答案只能是gcd的整数
倍。但是,这样考虑$x mod kx O((n + k)logv)$

奇奇怪怪的背包代码

upd: 假做法

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=1000005;
int a[maxn];
bool vis[maxn];
int main(){
    int n,k;scanf("%d%d",&n,&k);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=1;i<=n;++i)a[i]=a[i]%k;
    vis[0]=1;
    sort(a+1,a+n+1);
    for(int i=1;i<=n;++i){
        if(vis[a[i]])continue;
        bool flag=1;
        for(int j=a[i];flag||j!=a[i];j=(j+a[i])%k){
            flag=0;if(vis[j])continue;
            for(int x=j;x<k;++x){
                if(vis[x-j])vis[x]=1;
            }
        }
    }
    int ans=0;
    for(int i=0;i<k;++i)if(vis[i])++ans;
    printf("%d\n",ans);
    for(int i=0;i<k;++i)if(vis[i])printf("%d ",i);
    return 0;
}

真做法

code
#include<cstring>
#include<iostream>

using namespace std;

const int maxn = 1e6+55;

int gcd(int x, int y){
	return y == 0 ? x : gcd(y, x % y);
}

bool vis[maxn];

int main(){
	ios::sync_with_stdio(false);
	int n , k;
	cin >> n >> k;
	int gc ; cin >> gc;
	for(int i = 2; i <= n; ++i){
		int now ; cin >> now;
		gc = gcd(gc, now);
	}
	int now = gc % k;
	while(!vis[now]){
		vis[now] = 1;
		now += gc;
		now %= k;
	}
	int cnt = 0;
	for(int i = 0; i <= k; ++i)if(vis[i])++cnt;
	cout << cnt << endl;

	for(int i = 0; i <= k; ++i)if(vis[i])cout << i << " ";
	cout << endl;
	return 0;
}

B. biology

首先走的点越多越好,对于a相同的点,一定由a小于他们的最大的转移,可以列出DP式子f[i]=max(f[k]+b[i]+|x[i]x[k]|+|y[i]y[k]|),然后我们发现该式子转移最坏情况达到了O(N2M2),考虑如何优化

式子中最讨厌的就是绝对值,那么我们考虑拆掉,分四种情况讨论,然后可以开四个二维树状数组维护范围最大值转移,这样复杂度可以降到O(NMlogNlogM),可是貌似还会TLE,如何继续优化?

其实可以发现,假如我们不考虑状态是否合法,就是那个绝对值符号是否正确,因为不合法的相当于减去了一个数,合法状态加上了一个数,最终取的max一定是合法的状态,所以我们没有必要查询范围最值,直接使用全局最值是没有问题的,直接取最值即可


#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn=2005;
struct node{
    int x,y,a,b;
}c[maxn*maxn];
int tot,n,m;
bool cmp(node x,node y){
    return x.a<y.a;
}
void In(){
    scanf("%d%d",&n,&m);
    tot=0;
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j)
          scanf("%d",&c[++tot].a);
    tot=0;
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j)
            scanf("%d",&c[++tot].b);
    tot=0;
    for(int i=1;i<=n;++i)
     for(int j=1;j<=m;++j)
      {c[++tot].x=i;c[tot].y=j;}
    sort(c+1,c+tot+1,cmp);
}
long long f[maxn*maxn];
void Do(){
    int l=1;
    while(c[l].a==0)++l;
    int r=l;while(r<tot&&c[l].a==c[r+1].a)++r;
    for(int i=l;i<=r;++i)f[i]=c[i].b;
    long long ans=-1;
    while(r<tot){
        long long mx[4]={0,0,0,0};
        for(int i=l;i<=r;++i){
            mx[0]=max(mx[0],f[i]+c[i].x+c[i].y);
            mx[1]=max(mx[1],f[i]+c[i].x-c[i].y);
            mx[2]=max(mx[2],f[i]-c[i].x+c[i].y);
            mx[3]=max(mx[3],f[i]-c[i].x-c[i].y);
        }
        int now=c[r+1].a,nx=r+1;
        while(nx<=tot&&c[nx].a==now){
            f[nx]=c[nx].b+max(max(c[nx].x+c[nx].y+mx[3],c[nx].x-c[nx].y+mx[2]),max(c[nx].y-c[nx].x+mx[1],-c[nx].x-c[nx].y+mx[0]));
            ans=max(ans,f[nx]);
            ++nx;
        }
        l=r+1;
        r=nx-1;
    }
    printf("%lld\n",ans);
}
int main(){
    In();Do();
    return 0;
}
//主函数:您礼貌吗?

C. english

这题好难啊,用到了可持久化Tire树,启发式合并等我没学过的算法,对着题解一行行敲的,先简单理解一下,等学完了那些知识再看看有没有坑填

首先暴力代码+特殊性质特判可以搞到30-50pts

然后再说正解,用单调栈处理出每个数x作为最大值的区间,用l[x]r[x]记录区间左右端点,然后对以每个x单独处理

首先说对ans1的计算,假设确定了区间一侧端点,怎么快速求出所有从该端点出发的区间的贡献?因为异或运算的存在,考虑按位处理,如果端点的值该位为1,那么另一端为0才有贡献,反之亦然。预处理出前缀和数组sum[i][j]表示前i个数,第j位为1的个数,对于区间查询lxr为了降低循环次数,我们用x左右两端较小的来枚举,查询另一端与该位不同的的个数s,对答案有s(1<<j)a[x]的贡献。枚举求值即可

然后对于ans2我还没理解

对每个以x为最大值的区间,同样确定了一段,查找有多少有贡献的另一端,就是查找a[x]和某段区间中几个数异或值大于a[x],用类似前缀和的方式维护可持久化Tire树,每个数都是一个新的版本,我们只需要建一条链,剩下的查询历史版本即可。处理每个前缀的数的个数,即每个节点向下有几个有效信息。查找过程从高位向低位,如果a[x]此位为1,那a[i]与此位必须异或为1才可能贡献,如果a[x]此位为0,那么此位为异或为1的(前面与y相同)的数的个数就是贡献,然后还要继续向下累计异或为0的情况。最后用类似前缀和的方式减一下,防止负数加一个mod再计算即可。

#include<cstdio>
#include<cstring>

using namespace std;
const int mod=1e9+7;
const int maxn=2e5+25;
int max(int x,int y){return x>y?x:y;}
int a[maxn],n,opt;
int l[maxn],r[maxn],sta[maxn],top;
int sum[maxn][25],root[maxn];
long long ans1,ans2;
struct tire{
    int siz[maxn*30];//当前节点子树,有多少个信息(数)
    int son[maxn*30][2];//son[x][0/1] x 的两个子节点,0/1为边上维护的信息
    int cnt;
    void insert(int prt,int &rt,int x,int dep){//可持久化Tire树 prt历史版本根节点,rt当前状态根节点(未确定,需要传参),x插入的新信息(值),dep还需处理的深度
        rt=++cnt;siz[rt]=siz[prt];//开新点,个数记录历史版本
        if(dep<0){//处理完毕,加上本次贡献返回
            ++siz[rt];
            return;
        }
        int tmp=(x>>dep)&1;
        son[rt][tmp^1]=son[prt][tmp^1];//当前没有的点,继承历史版本
        insert(son[prt][tmp],son[rt][tmp],x,dep-1);//向下
        siz[rt]=siz[son[rt][0]]+siz[son[rt][1]];//统计个数
        return;
    }

    int query(int x,int y,int pos){//查询在1-pos区间内有多少数异或x大于y,即有贡献的个数
        int u=root[pos],ret=0;//ret统计答案,u为当前点
        for(int i=20;i>=0;--i){//从高位向低位找
            int a=(x>>i)&1;
            int b=(y>>i)&1;
            if(b==0){//如果y此位为0,那么此位为异或为1的(前面与y相同)的数的个数就是贡献
                ret+=siz[son[u][a^1]];//记录当前贡献
                u=son[u][a];//继续查找与y此位也相同,大于y的个数
            }
            else u=son[u][a^1];//如果y此位为1,那此位必须异或为1才可能贡献,向下求解
            if(!u)break;
        }
        return ret;
    }
}t;

void solve(int x,int l,int r){
    if(x-l<r-x){
        for(int i=l;i<=x;++i){
            for(int j=20;j>=0;--j){
                if((a[i]>>j)&1)ans1=(ans1+1ll*(1<<j)*(r-x+1-sum[r][j]+sum[x-1][j])%mod*a[x]%mod)%mod;
                else ans1=(ans1+1ll*(1<<j)*(sum[r][j]-sum[x-1][j])%mod*a[x]%mod)%mod;
            }
            ans2=(ans2+1ll*(t.query(a[i],a[x],r)-t.query(a[i],a[x],x-1)+mod)*a[x]%mod)%mod;
        }
    }else{
        for(int i=x;i<=r;++i){
            for(int j=20;j>=0;--j){
                if((a[i]>>j)&1)ans1=(ans1+1ll*(1<<j)*(x-l+1-sum[x][j]+sum[l-1][j])%mod*a[x]%mod)%mod;
                else ans1=(ans1+1ll*(1<<j)*(sum[x][j]-sum[l-1][j])%mod*a[x]%mod)%mod;
          }
          ans2=(ans2+1ll*(t.query(a[i],a[x],x)-t.query(a[i],a[x],l-1)+mod)*a[x]%mod)%mod;
        }
    }
}

int main(){
   //freopen("english3.in","r",stdin);
    //freopen("english.out","w",stdout);
    scanf("%d%d",&n,&opt);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]),t.insert(root[i-1],root[i],a[i],20);
    
    //单调栈维护x为最大值的区间,l[i],r[i]为区间左右端点
    for(int i=1;i<=n;++i){
        while(a[i]>=a[sta[top]]&&top)r[sta[top]]=i-1,--top;
        l[i]=sta[top]+1;
        sta[++top]=i;
    }
    while(top)r[sta[top]]=n,--top;

    //sum前缀和,sum[i][j]前i个数 从低向位第j位为1的有几个
    for(int i=1;i<=n;++i){
        for(int j=0;j<=20;++j){
            if(1&(a[i]>>j))sum[i][j]=sum[i-1][j]+1;
            else sum[i][j]=sum[i-1][j];
        }
    }

    for(int i=1;i<=n;++i)solve(i,l[i],r[i]);
    if(opt&1)printf("%lld\n",ans1);
    if(opt&2)printf("%lld\n",ans2);
    return 0;
}
posted @   Chen_jr  阅读(44)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示