USACO2023Feb游记

由于学校要求,过来打 USACO。

由于上次已经打到白金了,所以继续。

然后还是 AK 了。

感觉题意很迷惑,所以都翻译一下。

Hungry Cow

Bessie 很饿,每天晚饭如果有干草就会吃 \(1\) 份,没有就不吃,初始没有干草。
每天早上 Farmer John 会给它送若干干草,设第 \(k\) 天送 \(a_k\) 份干草,初始时 \(a_k=0\),表示该天不送干草。
\(q\) 次操作,每次给出 \(x,y\),表示将 \(a_x\) 改成 \(y\),请将在此时 Bessie 有干草吃的日期编号求和并输出。对 \(10^9+7\) 取模。
操作间互不独立。
\(1\le q\le10^5\)\(1\le x\le10^{14}\)\(0\le y\le10^9\)

注意到这个过程是一个“前时间”对“后时间”的贡献,考虑兔队线段树。

维护区间内有干草吃的编号数 \(cnt\)、编号和 \(ans\),以及对区间外的贡献 \(more\)

单点修改,全局查询。

考虑计算左侧区间对当前节点带来 \(c\) 的前缀贡献时,区间内三个值的值。

当填满左儿子空位时,就不用向左递归;否则,只用向左递归,右儿子结果和 \(c=0\) 时的右儿子结果一致。

总复杂度 \(O(q\log^2v)\)

核心代码:

const ullt Mod=1e9+7,inv2=5e8+4;
typedef ConstMod::mod_ullt<Mod>modint;
typedef std::vector<modint>modvec;
inline modint sum(ullt l,ullt r){return modint(r-l)*(l+r-1)*inv2;}
struct Seg{
    Seg*L,*R;ullt id,len,cnt,more;modint ans;
    Seg(ullt l,ullt r):L(NULL),R(NULL),id(l),len(r-l),cnt(0),more(0),ans(){}
    voi chg(ullt p,ullt v){
        if(len==1){
            if(v)cnt=1,more=v-1,ans=id;else cnt=more=0,ans=0;
            return;
        }
        if(p<(len>>1)){
            if(!L)L=new Seg(id,id+(len>>1));
            L->chg(p,v);
        }
        else{
            if(!R)R=new Seg(id+(len>>1),id+len);
            R->chg(p-(len>>1),v);
        }
        ans=L?L->ans:0,cnt=L?L->cnt:0;
        ullt Lv=L?L->more:0;
        auto g=R?R->cal(Lv):std::pair<modint,std::pair<ullt,ullt> >
            {sum(id+(len>>1),id+std::min(len,Lv+(len>>1))),
                {std::min(len-(len>>1),Lv),Lv-std::min(len-(len>>1),Lv)}};
        ans+=g.first,cnt+=g.second.first,more=g.second.second;
    }
    std::pair<modint,std::pair<ullt,ullt> >cal(ullt c){
        if(!c)return{ans,{cnt,more}};
        if(c+cnt>=len)return{sum(id,id+len),{len,c+cnt-len+more}};
        // printf("%llu %llu\n",id,id+len);
        auto g1=L?L->cal(c):std::pair<modint,std::pair<ullt,ullt> >
            {sum(id,id+std::min(len>>1,c)),{std::min(len>>1,c),c-std::min(len>>1,c)}};
        ullt Lv=g1.second.second;
        if(Lv==(L?L->more:0))return{ans-(L?L->ans:0)+g1.first,{g1.second.first+cnt-(L?L->cnt:0),more}};
        auto g2=R?R->cal(Lv):std::pair<modint,std::pair<ullt,ullt> >
            {sum(id+(len>>1),id+std::min(len,Lv+(len>>1))),
                {std::min(len-(len>>1),Lv),Lv-std::min(len-(len>>1),Lv)}};
        return{g1.first+g2.first,{g1.second.first+g2.second.first,g2.second.second}};
    }
};

Problem Setting

Farmer John 出了 \(n\) 道题,聘了 \(m\) 个验题人来品鉴难度。
难度只有简单(E)和困难(H)两种。
Farmer John 将从中选出若干道(至少一道),并以一定顺序排列,使得前一道题目中任意一个觉得此题困难的验题人也觉得后面一道题目困难。
回答有多少种选出来并排列的方案,对 \(10^9+7\) 取模。
\(1\le n\le10^5\)\(1\le m\le20\)

考虑把 E 视作 \(0\)H 视作 \(1\),就是相邻项为子集包含。

考虑把状态相同的数一起考虑,假设出现了 \(t\) 次,选择其的方案数即为

\[G_t=\sum_{k>0}k!\binom tk=\sum_{k>0}t^{\underline k}=t!\sum_{k<t}\frac1{k!} \]

可以 \(O(n)\) 预处理。

假设选的最后一个集合为 \(S\) 的方案数为 \(f_S\),集合为 \(S\) 的元素有 \(a_S\) 个,则

\[f_S=G_{a_S}(1+\sum_{T\subsetneq S}f_T) \]

直接子集枚举是 \(O(3^m)\) 的,不优,考虑更优做法。

注意到这个东西是一个半半在线卷积的形式,直接按 \(|S|\) 递增序进行 FMT 即可。其实就是高维前缀和。

总复杂度 \(O(nm+m^22^m)\),可以通过。

听说有 \(O(nm+m2^m)\) 做法,不懂哦,可能因为这里是子集和的形式吧。

核心代码:

const ullt Mod=1e9+7;
typedef ConstMod::mod_ullt<Mod>modint;
typedef std::vector<modint>modvec;
chr C[20][100005];uint Cnt[1u<<20|1],nLim;
modint P[100005],Q[100005],G[100005],Dp[21][1u<<20|1],User[1u<<20|1];
voi FWT(modint*x,bol op){
    for(uint i=1;i<nLim;i<<=1)for(uint j=0;j<nLim;j+=i<<1)for(uint k=0;k<i;k++)
        op?x[i+j+k]-=x[j+k]:x[i+j+k]+=x[j+k];
}
int main()
{
#ifdef MYEE
    freopen("QAQ.in","r",stdin);
    // freopen("QAQ.out","w",stdout);
#endif
    uint n,m;scanf("%u%u",&n,&m);
    for(uint i=0;i<m;i++)scanf("%s",C[i]);
    for(uint i=0;i<n;i++){
        uint v=0;
        for(uint j=0;j<m;j++)if(C[j][i]=='H')v|=1u<<j;
        Cnt[v]++;
    }
    P[0]=1;for(uint i=1;i<=n;i++)P[i]=P[i-1]*i;
    Q[n]=P[n].inv();for(uint i=n;i;i--)Q[i-1]=Q[i]*i;
    for(uint i=0;i<n;i++)G[i+1]=G[i]+Q[i];
    for(uint i=0;i<=n;i++)G[i]*=P[i];
    modint ans;
    nLim=1u<<m;
    for(uint j=0;j<=m;j++){
        for(uint k=0;k<(1u<<m);k++)if(__builtin_popcount(k)==j){
            Dp[j][k]=1;
            for(uint t=0;t<j;t++)Dp[j][k]+=Dp[t][k];
            ans+=Dp[j][k]*=G[Cnt[k]];
        }
        FWT(Dp[j],0);
    }
    ans.println();
    return 0;
}

Watching Cowflix

Bessie 喜欢在 Cowflix 上看节目,并且喜欢在农场里的不同地方看。
Farmer John 的农场可以被描述成一颗 \(n\) 个节点的树,并且 Bessie 只可能在树上的一些指定的节点处看节目。每个节点是否要看节目将在初始时给定;保证至少在一个节点处会看节目。
不幸的是,Cowflix 为了避免奶牛们使用公用账号,采取了一个新的会员策略:
Bessie 将多次付款,每次选择树上任意一个大小为 \(d\)联通块,为其支付 \(d+k\) 的代价,才能够在这些位置看节目。
换言之,Bessie 将选取若干联通块 \(c_1,c_2,\dots,c_C\),支付 \(\sum_{i=1}^C(|c_i|+k)\) 的代价,才可以在这些联通块的各个节点处看节目;即,被指定的节点必须被某个联通块包含,不被指定的节点不必被包含
Bessie 觉得这个策略的代价太昂贵了,考虑是否要改在 Mooloo 上看节目。为了帮助其决策,你应当告诉之 \(k\) 取遍 \(1\sim n\) 时看节目的最小总代价。
\(1\le n\le2\times10^5\)

先考虑 \(n\le5000\) 时怎么做。

我们对每个 \(k\) 考虑怎么暴力求答案。

考虑 dp,\(f_{p,0/1}\) 表示在 \(p\) 的子树中,当前根节点是否被选入某个联通块的最小代价;一个联通块额外的 \(k\) 的代价将在其根节点向父亲做贡献时计算。

\[f_{p,0}=\begin{cases}+\infty&p\text{ 必须被选入某个联通块}\\\sum_s\min\{f_{s,0},f_{s,1}+k\}&\text{otherwise.}\end{cases} \]

\[f_{p,1}=1+\sum_s\min\{f_{s,0},f_{s,1}\} \]

最后 \(\min\{f_{p,0},f_{p,1}+k\}\) 即为答案。

这样我们就得到一个 \(O(n)\) dp 的方法,对每个 \(k\) 暴力做一次,复杂度 \(O(n^2)\)

(不过想在 USACO 的老爷机上过 \(5000\) 还是比较困难的,可能要加一些常数优化)

接下来考虑怎么优化。

一种想法是 slope trick,但是这个东西非常没有前途,常数也不小。

考虑另一种方法:优化暴力!

注意到,\(k=n\) 时答案仍不超过 \(2n\),我们的答案不会很大;同时,容易发现答案关于 \(k\) 具有凸性

因此,我们的答案是一个关于 \(k\)凸壳

我们考虑把凸壳上的每一段相同的线段一起处理,其为等差数列

我们来证明一下凸壳上斜率不同的线段数目;显然斜率均为正整数

假设斜率为 \(k\) 的线段有 \(a_k\) 条,则

\[\sum_kka_k\le2n \]

于是 \(a_k>0\)\(k\)\(O(\sqrt n)\) 级别的!

考虑直接二分该等差数列在何处结束,复杂度为 \(O(n\sqrt n\log n)\) 的。

这种东西显然过不去,考虑优化一下复杂度。

我们把二分换掉,改成先倍增,倍增到不可行后再二分

这样,对斜率为 \(k\) 的线段,我们的查询轮数为 \(O(\log a_k)\) 的。

我们考虑分析一下 \(O(\sum_{a_k>0}\log a_k)\) 的级别。

我们把 \(\log a_k\) 描述为 \(\sum_t[2^t\le a_k]\),则即

\[O(\sum_{a_k>0}\log a_k)=O(\sum_{a_k>0}\sum_t[2^t\le a_k])=O(\sum_t\sum_{a_k\ge2^t}1) \]

由于

\[\sum_{a_k\ge w}k\le\sum_{a_k\ge w}k\lfloor a_k/w\rfloor\le\frac1w\sum_kka_k\le\frac{2n}{w} \]

因此满足 \(a_k\ge w\)\(k\)\(O(\sqrt{n/w})\) 级别的。

因此

\[O(\sum_t\sum_{a_k\ge2^t}1)=O(\sum_t\sqrt{\frac{n}{2^t}})=O(\sqrt n) \]

因此只用做 \(O(\sqrt n)\) 轮查询。

总复杂度即为 \(O(n\sqrt n)\)

这个东西如果实现的常数不够优秀,在 USACO 的老爷机上会 T!L!E!

因此考虑一个常数优化技巧:我们每轮 dp 进行 dfs 的常数太大了,且内存访问不连续,考虑预先进行一遍 dfs,把每个节点重标号一下,使得父亲标号小于当前标号,这样每次 dp 只用进行一轮循环即可了。

实践表明,这样的常数大约是原来的 \(1/6\) 左右,可以轻松通过。

核心代码:

std::vector<uint>Way[200005];
chr C[200005];
uint Dp[2][200005];
uint Dfn[200005],Fath[200005],cnt;
bol Op[200005];
voi dfs0(uint p,uint f){
    Fath[Dfn[p]=cnt]=~f?Dfn[f]:-1u,Op[cnt++]=C[p]=='1';
    for(auto s:Way[p])if(s!=f)dfs0(s,p);
}
std::map<uint,uint>M;
uint find(uint v){
    if(M.count(v))return M[v];
    for(uint i=0;i<cnt;i++)Dp[0][i]=0,Dp[1][i]=1;
    for(uint i=cnt-1;i;i--){
        if(Op[i]){
            Dp[0][Fath[i]]+=Dp[1][i]+v;
            Dp[1][Fath[i]]+=Dp[1][i];
        }
        else{
            Dp[0][Fath[i]]+=std::min(Dp[0][i],Dp[1][i]+v);
            Dp[1][Fath[i]]+=std::min(Dp[0][i],Dp[1][i]);
        }
    }
    if(Op[0])Dp[0][0]=1e9;
    return M[v]=std::min(Dp[0][0],Dp[1][0]+v);
}
uint Ans[200005];
int main()
{
#ifdef MYEE
    freopen("QAQ.in","r",stdin);
    // freopen("QAQ.out","w",stdout);
#endif
    uint n;scanf("%u%s",&n,C);
    for(uint i=1,u,v;i<n;i++)scanf("%u%u",&u,&v),Way[--u].push_back(--v),Way[v].push_back(u);
    // printf("%u\n",find(1));
    dfs0(0,-1);
    for(uint l=1;l<=n;){
        Ans[l]=find(l);
        if(l==n)break;
        Ans[l+1]=find(l+1);
        if(Ans[l+1]==Ans[l]+1){
            for(uint j=l+2;j<=n;j++)
                Ans[j]=Ans[j-1]+1;
            break;
        }
        uint len=2;
        while(l+len<=n){
            if(find(l+len)-Ans[l]!=(Ans[l+1]-Ans[l])*len)
                break;
            len<<=1;
        }
        uint p=l+(len>>1);
        while(len>1){
            uint mid=p+(len>>=1);
            if(mid<=n&&find(mid)-Ans[l]==(Ans[l+1]-Ans[l])*(mid-l))p=mid;
        }
        for(uint j=l+2;j<=p;j++)Ans[j]=Ans[j-1]*2-Ans[j-2];
        l=p+1;
    }
    for(uint i=1;i<=n;i++)printf("%u\n",Ans[i]);
    return 0;
}
posted @ 2023-03-01 07:44  myee  阅读(278)  评论(1编辑  收藏  举报