[Codeforces348C]Subset Sums(乱搞)

=== ===

这里放传送门

=== ===

题解

这玩意儿算啥算法啊。。说它是乱搞吧,它其实也挺机智的;但是好像ATP知道的任何一种除了乱搞以外的算法都不能描述它。。。

这道题的意思是给定一个数列和 m 个集合,集合总大小也不超过m。每个集合里都指定了数列里的一些位置。要求支持两种操作,一个是把某个集合中的所有位置的数字都加上某一个值,一个是查询某个集合里的位置上的数字之和。
这题的难点就在于对一个集合的修改可能会影响到好多别的集合的数字和,关键就是怎么解决这个问题。然而我们可以发现因为题目中告诉我们集合总大小不超过 m ,那么大小超过m的集合最多只有 m 个。而对于那些大小小于 m 的集合我们可以直接枚举里面的元素暴力修改暴力查询,复杂度不会超过 O(m)
然后考虑如果只有大小大于 m 的集合的话我们该怎么做,设这一类集合的个数为 S ,可以维护一个O(S×S)的数组来记录每个集合和别的集合有多少个元素是重合的,然后再维护一个 O(S) 的数组来记录每个集合的增量。这样的话修改首先是 O(1) 的,然后对于查询的话我们可以枚举别的集合来计算那个集合对于当前查询的集合的影响,也就是如果当前集合是 A ,我们枚举所有除了A以外的集合 B ,那么A的元素和就要累加 deltaB×|AB|

那把这两种情况结合一下就有一种科学的做法了。设大小小于 m 的集合为S类集合,个数为 |S| ;大于 m 的集合为B类集合,个数为 |B|
修改S类集合的时候首先暴力修改原序列以方便S类集合的求和操作,然后要考虑对B类集合造成的影响。这个时候我们预处理一个大小为 (|S|+|B|)×|B| 的数组来记录每个B类集合和其它所有集合有多少交集,然后对于每个B类集合维护一个数组记录它的元素和,修改的时候根据那个记录的数组来更新元素和就可以。修改B类集合的时候直接维护它的增量就可以了。
对于S类集合的求和操作,先枚举原序列中的元素,这样可以统计S类集合的修改对它造成的影响;因为B类集合的增量没有累加到原序列里,所以枚举所有B类集合,把增量*交集大小累加到答案里。对于B类集合的求和操作,因为S类集合的增量已经累加过来了,所以直接枚举其它的B类集合累加增量就可以了。

可以发现对于S类集合的操作是和它的集合大小成正比的,对于B类集合的操作是和它的集合个数成正比的。总复杂度是 O(mm)

代码

#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,q,tot,rec[100010],cnt[100010][360],num[360],Limit,W,tmp[100010];
long long delta[360],sum[360],a[100010];
struct Set{
    int k,l,r;
}s[100010];
long long AskBig(int now){
    int pos=tmp[now];
    long long ans=sum[pos];
    for (int i=1;i<=W;i++)
      ans+=(long long)cnt[now][i]*delta[i];
    return ans;
}
long long AskSmall(int now){
    long long ans=0;
    for (int i=s[now].l;i<=s[now].r;i++)
      ans+=a[rec[i]];
    for (int i=1;i<=W;i++)
      ans+=(long long)cnt[now][i]*delta[i];
    return ans;
}
void ChangeSmall(int now,int v){
    for (int i=s[now].l;i<=s[now].r;i++)
      a[rec[i]]+=v;
    for (int i=1;i<=W;i++)
      sum[i]+=(long long)cnt[now][i]*v;
}
int main()
{
    scanf("%d%d%d",&n,&m,&q);
    Limit=ceil(sqrt(n));
    for (int i=1;i<=n;i++) scanf("%I64d",&a[i]);
    for (int i=1;i<=m;i++){
        int k;scanf("%d",&k);
        s[i].k=k;s[i].l=tot+1;
        for (int j=1;j<=k;j++){
            int x;scanf("%d",&x);
            rec[++tot]=x;
        }s[i].r=tot;
        if (k>Limit) num[++W]=i;
    }
    for (int i=1;i<=W;i++){
        int u=num[i];
        for (int j=s[u].l;j<=s[u].r;j++){
            ++tmp[rec[j]];
            sum[i]+=a[rec[j]];
        }
        for (int j=1;j<=m;j++)
          for (int k=s[j].l;k<=s[j].r;k++)
            cnt[j][i]+=tmp[rec[k]];
        for (int j=s[u].l;j<=s[u].r;j++) --tmp[rec[j]];
      }
    for (int i=1;i<=W;i++) tmp[num[i]]=i;
    for (int i=1;i<=q;i++){
        char c=getchar();
        int x,v;
        while (c!='?'&&c!='+') c=getchar();
        if (c=='?'){
            scanf("%d",&x);
            if (s[x].k>Limit)
              printf("%I64d\n",AskBig(x));
            else printf("%I64d\n",AskSmall(x));
        }else{
            scanf("%d%d",&x,&v);
            if (s[x].k>Limit) delta[tmp[x]]+=v;
            else ChangeSmall(x,v);
        }
    }
    return 0;
}

偏偏在最后出现的补充说明

这种按照大数据和小数据分类讨论的思路有时候可以解决一些用普通方法很难解决的问题。考虑的时候要注意对于两种类型的元素的特点设计复杂度合适的算法,每一种类型的元素进行操作的时候都要考虑另外一种元素的影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值