Loading

【题解】2021.1.22 至 2021.1.29 简要做题记录

自然是不包括所有题 = =

仅作 \(2020.1.22\)\(2020.1.29\) 的做题记录。

注意,更新顺序是从上往下,即最新的在下面 /wq

大概都是 cf,有些没写题解的题也懒得写了,唉。


CF1473G [2800,*medium,组合数学+NTT]

首先这个绝对值的限制就保证了块数在一定限制内。

\(f(l,r,x,y)\) 表示当前从 \(l\) 个块中的第 \(x\) 个移动到 \(r\) 个块中的第 \(y\) 个,考虑这个 \(f(l,r,x,y)\) 怎么算。首先考虑 \(l<r\) 的情况,容易发现每一次每个块都有两种走法,也仅有两种走法。按理来说,前一列中的第 \(i\) 块可以走到下一列的第 \(i\) 块和第 \(i+1\) 块,那这样的话就有:

\[f(l,r,x,y)={r-l\choose y-x}\quad \text{if}\ l<r\\ \]

然后考虑 \(l>r​\) 的情况,容易发现将路径反过来统计即可。

接着可以发现,按照上式计算,\(r-l\) 其实就是中间的块数(也就是 \(a,b\)),因此可以直接省掉。然后就可以拿一个 \(dp\) 了:令 \(dp(i,j)\) 表示做完了前 \(i\) 段,现在在第 \(j\) 块的方案数。

观察这个转移式,可以发现这就是个赤裸的卷积了吧。然后就没了,但是这样做有点卡不过去。

考虑把这个 \(dp(i,j)\) 列出来:

\[dp(i,j)=\sum_{k}dp(i-1,k)\left(\sum_{l}{a\choose l-k}{b\choose l-j}\right) \]

这里 \(l\) 枚举的是中转点的位置,换个法子枚举,枚举 \(k\) 向下的步数:

\[dp(i,j)=\sum_{k}dp(i-1,k)\left(\sum_{l}{a\choose l}{b\choose k+l-j}\right) \]

然后可以发现 \({b\choose k+l-j}={b\choose b-k-l+j}\) ,这就是个范德蒙德卷积的形式了:

\[dp(i,j)=\sum_{k}dp(i-1,k){a+b\choose b-k+j} \]

这后面的东西卷一下就没了。因为这样只需要考虑两侧的情况,这样子的话因为 \(|a_i-b_i|\leq 5\),所以多项式长度就是 \(O(n)\) 而非 \(O(m)\) 的。所以复杂度是 \(O(n^2\log m)\) 而不是 \(O(nm\log m)\),足以通过此题。

const int N=2e3+5;
const int M=2e5+5;
const int LogN=20;

int n,a[N],b[N],fac[M],inv[M];

inline void init(int L=M-5) {
    fac[0]=1;
    lep(i,1,L) fac[i]=mul(fac[i-1],i);
    inv[L]=modpow(fac[L],-1);
    rep(i,L,1) inv[i-1]=mul(inv[i],i);
}
inline int C(int n,int m) {
    if(n<0||m<0||n<m) return 0;
    return mul(fac[n],mul(inv[m],inv[n-m]));
}

poly dp,tmp;
int main() {
    IN(n),init(),init_w();
    lep(i,1,n) IN(a[i],b[i]);
    
    int ans=0,now=1; dp.rez(2),dp[1]=1;
    lep(i,1,n) {
        int l=now,t=now+a[i],r=now+a[i]-b[i]; now=r;
        tmp.clear(),tmp.rez(l+r+1);
        lep(j,1,l+r) tmp[j]=C(a[i]+b[i],b[i]+j-l); dp=dp*tmp;
        lep(j,1,r) dp[j]=dp[j+l]; dp.rez(r+1);
    }
    lep(i,1,now) pls(ans,dp[i]); printf("%d\n",ans);
    return 0;
}

CF1461F [2700,*hard,分内讨论+发掘性质+DP]

分内讨论:

  • 一个符号:

    显然那就只能全填是吧。

  • 两个符号:

    • +-:是个傻子都是直接填加号。
    • *+:额外情况。
    • *-:显然答案下限是 \(0\) 。对于从前往后的第一个 \(0\) ,显然前面都是填乘号,然后这个 \(0\) 前面是一个减号,后面的就都是乘号了。这样子的话后面的贡献就是 \(0\)。显然这是最优的情况,因为只有第一个数前面自带一个加号。
  • 三个符号:

    • *+-:显然减号完全没有用武之地,因此也属于额外情况。
  • 额外情况(有 *+):

    这个是本题的重点了。

    首先,对于所有的 \(0\) ,显然这个 \(0\) 前后都是填 + 。这样,这么多 \(0\) 就把原序列分成了若干段。接下来只需要考虑同一段内的。显然这并不是直接乘起来这么简单,如果有 \(1\) 的话,乘起来不一定是最优的。

    那么这下这么多 \(1\) 又把一段分成了若干区间,显然这么多区间里面都是 * ,问题就在于,每个 \(1\) 的前后,到底是 + 还是 *

    然后可以发现,最差的情况莫过于,中间有一堆连续的 \(1\) ,然后两边是否需要连起来。很显然,如果两边乘在一起大于 \(lim\) ,而两边加在一起在加上中间的 \(1\) 的和小于等于 \(lim\) ,则必然是将它们连起来。因为 \(1\) 最多 \(10^5\) 个,考虑最劣情况,即有一边是 \(2\) ,那么如果另外一边的乘积大于等于 \(10^5+3\) ,这样乘在一起就是 \(2\times 10^5+6\) 了,加在一起却是 \(2\times10^5+5\),所以这个 \(lim\) 其实是 \(10^5\) 级别的。

    那么考虑一个 \(dp\)\(dp(i)\) 表示前 \(i\) 个区间的最优值(显然这个 \(dp(i)\leq lim\),因此可以直接记录),枚举上一次填加号的地方转移即可。

    然后可以发现这个 \(dp\) 其实是 \(O(n^2)\) 的,但是如果整段区间的积已经大于 \(lim\) 了,那么一定是全部填 * 最优,因此即使要进行 \(dp\) ,剩下的乘积也是小于 \(lim\) 的,也就是说,这个 \(dp\) 其实是 \(O(\log^2 lim)\) 的。

细节挺多,代码挺长,就只贴"额外情况"的代码好了。

const int N=1e5+5;
const int lim=2e5;

int n,a[N];
char str[N];

bool work;
int dp[N],pre[N],sgn[N],val[N],L[N],R[N];
inline void build(int cnt,int l,int r) {
    L[cnt]=l,R[cnt]=r,val[cnt]=1,pre[cnt]=0,dp[cnt]=-1;
    if(!work) return ;
    lep(i,l,r) val[cnt]*=a[i]; int tmp=1;
    rep(i,cnt,1) {
        tmp*=val[i];
        if(dp[cnt]<dp[i-1]+L[i]-R[i-1]-1+tmp) dp[cnt]=dp[i-1]+L[i]-R[i-1]-1+tmp,pre[cnt]=i;
    }
}
inline void solve(int l,int r) {
    if(l>r) return ;
    int tmp=1,cnt=0; work=true;
    lep(i,l,r) if(tmp*=a[i],tmp>lim) {work=false; break;}

    std::vector <int> sta;
    lep(i,l,r) if(a[i]==1) sta.pb(i); int T=sta.size()-1;
    if(T==-1) {lep(j,l,r-1) printf("%d*",a[j]); printf("%d",a[r]); return ;}

    R[0]=l-1;
    if(l<sta[0]) build(++cnt,l,sta[0]-1);
    for(int i=0;i<T;++i) if(sta[i]+1<sta[i+1]) build(++cnt,sta[i]+1,sta[i+1]-1);
    if(r>sta[T]) build(++cnt,sta[T]+1,r);
    if(!work) pre[1]=0,pre[cnt]=1;

    lep(i,l,L[1]-1) sgn[i]=1;
    lep(i,R[cnt],r) sgn[i]=1;
    int now=cnt,las=pre[now];
    while(now) {lep(i,R[las-1],L[las]-1) sgn[i]=1; now=las-1,las=pre[now];}
    lep(i,l,r-1) printf("%d%c",a[i],sgn[i]==1?'+':'*'); printf("%d",a[r]); 
}
inline void extra() {
    std::vector <int> sta;
    lep(i,1,n) if(a[i]==0) sta.pb(i); int T=sta.size()-1;
    if(T==-1) return solve(1,n),puts(""),void();
    solve(1,sta[0]-1);
    for(int i=0;i<T;++i) {
        if(sta[i]>1) printf("+"); printf("0"); if(sta[i]<n&&sta[i]<sta[i+1]-1) printf("+");    
        solve(sta[i]+1,sta[i+1]-1);
    }
    if(sta[T]>1) printf("+"); printf("0"); if(sta[T]<n) printf("+");
    solve(sta[T]+1,n),puts("");
}

CF1458C [2700,*easy,发掘性质]

发掘一下性质,这个 IC 十分的神奇。

首先,令 \((i,j,x)\) 表示 \(i,j\) 上的值是 \(x\) ,容易发现,对于 I ,其实就是将 \((i,j,x)\) 变为 \((i,x,j)\) ,同样对于 C,其实就是将 \((i,j,x)\) 变成 \((x,j,i)\)

然后对于 UDLR,其实就是将 \(i\) 变一下或者 \(j\) 变一下。

可以将 IC 看作换个位置,然后将 UDLR 看作修改值,这样子的话,因为每次操作都是全体操作,所以只需要维护位置即可,然后记录一下每个位置的修改值是多少即可。这个每次修改就是 \(O(1)\) 的,最后重组矩阵是 \(O(n^2)​\) 的,足以通过此题。

const int N=1e3+5;

int n,m,mat[N][N],ans[N][N];
char str[N];

inline void solve() {
    IN(n,m);
    lep(i,0,n-1) lep(j,0,n-1) IN(mat[i][j]),--mat[i][j];
    scanf("%s",str+1); int q=strlen(str+1);

    int p1=1,p2=2,p3=3; int t[4]={0,0,0,0},o[4]={0,0,0,0}; lep(i,1,q) {
        if(str[i]=='U') --t[p1]; if(str[i]=='D') ++t[p1];
        if(str[i]=='L') --t[p2]; if(str[i]=='R') ++t[p2];
        if(str[i]=='I') swap(p2,p3); if(str[i]=='C') swap(p1,p3);
    }
    t[1]%=n,t[2]%=n,t[3]%=n;

    lep(i,0,n-1) lep(j,0,n-1) {
        o[1]=(i+t[1]+n)%n,o[2]=(j+t[2]+n)%n,o[3]=(mat[i][j]+t[3]+n)%n;
        ans[o[p1]][o[p2]]=o[p3];
    }
    lep(i,0,n-1) {lep(j,0,n-1) printf("%d ",ans[i][j]+1); puts("");}
    puts("");
}

int main() {
    int T=int(IN);
    while(T--) solve();
    return 0;
}

CF1455G [2900,*medium,建树+线段树合并优化 DP]

首先可以想到建树。

\(dp(i,j)\) 表示走完 \(i\) 的子树,出来的时候为 \(j\) 的最小代价。显然因为只有 if 节点有儿子,所以进入 \(i\) 子树的值是唯一确定的。

用线段树维护 \(dp(i,j)\) 。对于 \(i\) 的一个儿子 \(u\) ,如果 \(u\)set ,那么就只需要支持单点修改和区间修改(还要维护区间 \(\min\)),如果 \(u\)if ,那么求出 \(dp(u,j)\) ,然后做区间加(进入 \(u\) 的代价)后线段树合并即可。

线段树合并的时间复杂度显然正确:一个 set 操作最多会使不同数字集合的大小加一。

然后这题就做完了。(建树好像有一堆细节要讨论,不想写了。


CF1455F [2800,*medium,字符串+发掘性质]

首先一个字符最多被操作 \(1\) 次,这意味着一个字符最多被移动到前面两格。

\(str(i)\) 表示前 \(i\) 个字符的最小字典序,转移的话依次考虑 \(i\) 的新位置为 \(i,i-1,i-2\) 的情况即可。


CF1468B [2900,*hard,单调性+数据结构]

发现这个结构很像一个堆栈。每一天丢进去几个,然后再取出几个栈顶。

假设第 \(i\) 天没有卖完,一直堆在栈中,到第 \(j\) 天才卖完。那么 \([i,j]\) 中间的所有天的所有面包,都可以在该段天数区间内卖完。因为 \(i\) 对于 \(k>i\)\(i\) 在栈中的元素一定比 \(k\) 在栈中的元素距离栈顶远。

那么我们称这个区间 \([i,j]​\) 为一个"可以独立自主解决问题的区间"。容易发现,只需要找到这些区间,然后答案就是最长的区间的长度了。接下来的问题就是怎么找这些区间。

考虑 \(k\)\(10^9\) 开始慢慢减少。最开始的时候显然区间都是 \([i,i]\) 的形式。慢慢减少 \(k\) 的同时,会发生:

  • 一个区间的面包卖不完了,将其与下一个区间合并。

现在来考虑这个合并的时间。首先需要注意几点:

  • 如果拿一个空栈从一个区间的左端点做到右端点,除了到右端点,其他的时刻一定不存在空栈,否则区间可以被分成两半。这就意味着没有一次弹栈机会会被浪费
  • 当前一个区间有剩下的面包留到接下来一个区间时,因为剩下的面包是在栈底的,因此一定不会对这个区间的结构造成影响。

因为没有弹栈机会被浪费,所以一个区间可以接受的最小 \(k\) 其实就是 \(\frac{\sum a_i}{len}\) ,将其定义为区间的限制。

这下子用一个 set 维护就好了,每一次找到区间限制最大的区间,然后将其与下一个区间合并即可。

const int N=2e5+5;

ll a[N],sum[N];
int n,m,q[N],L[N],R[N];
struct Node {
    int l,r; ll val;
    bool operator < (const Node &ele) const {return val>ele.val||(val==ele.val&&r>ele.r);}
}; std::set <Node> S;

inline ll calc(int l,int r) {return (sum[r]-sum[l-1]+r-l)/(r-l+1); }
inline Node merge(Node a,Node b) {
    R[a.l]=b.r,L[b.r]=a.l;
    return (Node){a.l,b.r,calc(a.l,b.r)};
}

int ans[N],qwq[N],tim[N];
int main() {
    IN(n,m);
    lep(i,1,n) IN(a[i]); a[n]=-2e18;
    lep(i,1,n) L[i]=R[i]=i,S.insert((Node){i,i,a[i]});
    lep(i,1,n) sum[i]=sum[i-1]+a[i];

    int c=0; tim[0]=inf;
    while(S.size()>1) {
        Node now=*S.begin(),nxt=(Node){now.r+1,R[now.r+1],calc(now.r+1,R[now.r+1])};
        S.erase(now),S.erase(nxt),S.insert(merge(now,nxt));

        if(now.val<tim[c]) ++c,qwq[c]=max(qwq[c-1],nxt.r-now.l),tim[c]=now.val;
        else chkmax(qwq[c],nxt.r-now.l);
    }
    lep(i,1,c) --tim[i];
    std::reverse(tim+1,tim+1+c),std::reverse(qwq+1,qwq+1+c);
    lep(i,1,m) IN(q[i]),printf("%d ",qwq[lower_bound(tim+1,tim+1+c,q[i])-tim]);
    return puts(""),0;
}

CF1450H [2900/3300,*hard]

考虑最优的方案一定是两个连在一起就消掉,然后最后剩下的一定是一个 01 交错的序列,答案就为这个序列的长度除四,也就是 01 的个数除二。

怎么方便的求出这个东西?考虑原序列,一定是 0 段和 1 段交错的形式。现在考虑统计最终的 01 交错序列的 0 的个数。首先,对于长度为偶数的 0 段,全部都做不出贡献。其次,对于长度为奇数的 0 段,如果要做出贡献,必须满足两个奇数段不能合并到一块去,如果要满足不合并到一块去就必须满足这两段之间有奇数个元素。

也就是说,如果两个奇数段中间有偶数个元素,那么无论如何它们都会合并到一块儿去。

现在考虑对下标按照奇偶分类,容易发现,对于偶数段,下标为奇数的和下标为偶数的个数一样,对于可以合并的两个奇数段,将奇数下标和偶数下标加起来后发现他们仍然相等。对于无法合并的两个奇数段,奇数下标的个数和和偶数下标的个数和总是相差 \(2\),而这两个奇数段又会同时做出贡献。

因此可以发现,如果令奇数下标的 0\(a\) 个,偶数下标的 0\(b\) 个,那么答案就是 \(\frac{|a-b|}{2}\)

现在考虑有 \(x\) 个奇数下标的 ?,有 \(y\) 个偶数下标的 ?,假设从 \(x\) 里面找出来 \(i\)0\(y\) 里面找出来 \(j\)1 ,然后答案就是 \(\frac{|a+i-b-j|}{2}\),现在令人头疼的就是这个绝对值怎么去掉。

首先求一个东西:\(i-j=k\) 的方案数:

\[\begin{aligned} f(k)&=\sum_{i=0}^{x}{x\choose i}{y\choose i-k}\\ &=\sum_{i=0}^{x}{x\choose i}{y\choose y-i+k}\\ &={y+x\choose y+k}\\ \end{aligned} \]

容易发现 \(-y\leq k\leq x\) ,枚举这个 \(k\) 的情况带入答案式即可,答案即为(令 \(t=a-b\)):

\[ans=\frac{1}{2^{x+y-1}}\sum_{k=-y}^{x}[t+k\equiv0\bmod{2}]{y+x\choose y+k}\frac{|t+k|}{2} \]

这个就是 \(O(n)\) 的了,足以通过 easy version 。考虑怎么修改,首先这个位置不用管吧,只需要看位置的奇偶性了。如果修改的是 0 ,其实就只需要改 \(t\) 的值即可。如果修改的是 ? ,就修改 \(x,y\) 即可。

重新定义 \(t=y+b-a\) ,令 \(p=x+y\)

\[\begin{aligned} ans&=\frac{1}{2^{p}}\left(\sum_{k=t+1,k\equiv t\bmod{2}}^{p}{p\choose k}(k-t)+\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}(t-k)\right)\\ &=\frac{1}{2^{p}}\left(\sum_{k=t+1,k\equiv t\bmod{2}}^{p}{p\choose k}k-t\sum_{k=t+1,k\equiv t\bmod{2}}^{p}{p\choose k}+t\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}-\sum_{k=0,k\equiv t\bmod{2}}^{ti}{p\choose k}k\right)\\ &=\frac{1}{2^{p}}\left(\sum_{k=0,k\equiv t\bmod{2}}^{p}{p\choose k}k-t\sum_{k=0,k\equiv t\bmod{2}}^{p}{p\choose k}+2t\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}-2\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}k\right)\\ &=\frac{1}{2^{p}}\left(p2^{p-2}-t2^{p-1}+2t\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}-2\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}k\right)\\ &=\frac{p-2t}{4}+\frac{1}{2^{p-1}}\left(t\sum_{k=0,k\equiv t\bmod{2}}^{t}{p\choose k}-p\sum_{k=0,k\not \equiv t\bmod{2}}^{t-1}{p-1\choose k}\right)\\ &=\frac{p-2t}{4}+\frac{1}{2^{p-1}}\left(t\sum_{k=0,k\equiv t\bmod{2}}^{t}{p-1\choose k}+(t-p)\sum_{k=0,k\not \equiv t\bmod{2}}^{t-1}{p-1\choose k}\right)\\ \end{aligned} \]

定义 \(f(n,t)​\)

\[\begin{aligned} f(n,t)&=\sum_{i=0,i\equiv t\bmod{2}}^{t}{n\choose i}\\ &=\sum_{i=0,i\equiv t\bmod{2}}^{t}{n-1\choose i}+\sum_{i=0,i\equiv (t-1)\bmod{2}}^{t-1}{n-1\choose i}\\ &=\sum_{i=0}^{t}{n-1\choose i}\\ ans&=\frac{p-2t}{4}+\frac{1}{2^{p-1}}\left(tf(p,t)-pf(p-1,t-1)\right)\\ \end{aligned} \]

因为每次 \(p,t\) 的变化量只有 \(1\),所以这个 \(f(p,t)\) 显然可以 \(O(1)\) 更新。

边界细节巨 tm 多,吐了。


CF1326F [2600/3200,*hard]

这题我感觉挺牛逼的,记录一下。F1 没多想,直接开的 F2 。

此题关键:通过容斥可以将不同的状态数从 \(2^{n-1}\) 变为 \(P(n)\),实现状态数大幅度降低。
常见的容斥也多可以用来实现降低状态数。

以下为正文部分。

观察一下一个序列代表着什么。首先,如果两个 0 连在一起,则说明中间这个点是单独的一个;如果两个 0 中间有一个 1 ,这说明中间这两个点有一条边。所以,连续的 \(k\)1 就代表着一个长度为 \(k+1\) 的链。

考虑容斥,钦定位置集合 \(S\) 上都是 1 ,其余位置随便的方案数为 \(f(S)\)。这样的话将 \(S\) 的链集合求出来后,只需要满足链集合成立,而并不需要关系链集合之间是否成立。容易发现,位置集合 \(S\) 对印着原图的链大小集合 \({a_1,a_2,\cdots,a_k}\) ,显然有 \(\sum a_i=n\) ,容易发现因为 \(n=18\),链大小集合的方案数实质上是 \(n\) 的划分数,因此它其实最多就是 \(385\) 。也就是说,最多有本质不同 \(385\)\(f(S)\) ,这下就好做多了。

然后接下来考虑如何对这 \(385\)\(f(S)\) 求解。

\(2021.1.29\) :暂时留坑。


Contest 1466 (Good bye 2020),[800~3400]

A [*easy]

暴力枚举另外两个端点即可。

B [*easy]

从大数到小数贪心即可。

C [*easy]

只要不出现形如 babbb 的串就行,\(dp\) 即可,记录前两个字符。

D [*easy]

\(n-1\)\(1\) 做,容易发现每一次会消掉一个点的贡献,每次找权值最小的可以且还可以做贡献的点即可,这个可以用数据结构随便维护。

E [*easy]

\(a_j\) 处统计,就是要求全局与 \(a_j\)\(\&\) 和以及 \(|\) 和,这个只需要将位拆开统计。

F [*medium]

将每一个维度看成点,然后向量就是连边。

容易发现对于一条长度为 \(n\) 的链,其不同的状态数有 \(2^{n-1}\) 种,如果再连一条边变成环的话,状态数就有 \(2^{n}\) 种,但是不同的状态数仍然只有 \(2^{n-1}\) 。这说明环边都是没有用的。

如果从这里出发的话会发现第一个样例不好解释,因为是自环。不妨考虑建一个虚点,对于只有一个维度上有 \(1\) 的向量相当于将位置和虚连边。这样就解释的通了。

这个用并查集维护即可。对于 \(|T|\) 的话,直接用 \(|S'|\) 求即可。

G [*medium]

首先可以分治,将 \(s_i\) 的答案分成在 \(s_{i-1}\) 的部分和跨过 \(t_{i-1}\) 的部分。

\(s_{i-1}\) 的部分递归,然后跨过 \(t_{i-1}\) 的部分直接找询问串 \(q\) 中的相同字符,这个时候再维护一下 \(s_{i-1}\) 的前后缀哈希就可以算贡献了。由于 \(|q|\leq 10^6\),因此维护前后缀哈希也就只需要维护 \(10^6\) 位。

然后容易发现,当 \(|s_{i-1}|\geq 10^6\) 时,对于 \(s_{i}\) 的前后缀哈希,可以直接继承 \(s_{i-1}\) 的。这可以启发我们,也许根本不要在意是 \(s_{i}\) 还是 \(s_{i-1}\) ,反正后面的前后缀哈希都一样,需要在意的仅有中间的字符 \(t\) ,而不同的 \(t\) 只有 \(26\) 种,于是这题就比较可做了。

首先,处理出所有的 \(|s_{i-1}|<{10^6}\)\(s_i\) 并做好前后缀哈希,因为每次字符串长度一定翻倍所以显然这样的 \(i\)\(\log\) 级别的,令 \(lim\) 是满足条件的最大 \(i\)

然后询问时的 \(k\) ,对于 \(k>lim\) ,接下来的串一定是有 \(2\) 的若干次方的 \(s_{lim}\) 出现,然后开个桶记录一下这中间的 \(t\) 的个数,枚举 \(t\) 分别统计即可。

对于 \(q\)\(s_{lim}\) 中的出现次数,因为维护了前后缀哈希,所以可以很方便的求出了。(当然也可以用别的方法 ...

Contest 1375 (Codeforces Global Round 9),[1100~3500]

E [*medium]

考虑逐位还原,对于当前的第 \(i\) 位,找到与其形成逆序对的位置,然后显然最后要把最小的那个换到第 \(i\) 位,并且不破坏后面的大小关系。

显然先换大数再换小数是最优策略。对于值相同的数也需要分出个大小关系,这个最好在外面就处理好,因为在里面排序的时候再判大小关系可能会出错。

F [*hard]

发现先手必胜的状态就是,将三堆石子从小到大排序后,大小是形如 \(a,a+k,a+2k\) 的,并且上一次被操作过的是第三堆。假设做到了这么一个局面,但是上一次操作的并不是最大的那个,就可以给一个 \(3k\) ,那无论怎么操作都避免不了这种必败局面了。

事实上,我们有一个更好的方法,对于 \(a,b,c\) ,只需要给一个 \(2c-a-b\) ,这个时候只能加在 \(c\) 上面,否则就会直接变成必败局面。显然,加在 \(c\) 上后,下一次操作就不能加在 \(c\) 上了,这个时候就是一个妥妥的必败局面了。

G [*medium]

这题其实挺普通的。

首先很显然不会有无用操作,也就是说确立一个点为最终的菊花中心后,所有的操作一定是关于这个点的。这个时候,假设已经确定了菊花中心,将其立为根,通过画图可以很容易发现:

  • 儿子节点到该节点的边:这种边不用消掉。
  • 孙子节点到儿子节点的边:以儿子节点为 \(b\),这种边是需要消掉的,消掉后原孙子变成新儿子。
  • 曾孙节点到孙子节点的边:操作孙子节点的时候是会将这些点全部连到根的,因此这种边不用消掉。
  • ......

如此看来,一个边需不需要消掉,跟其到根的距离有关系。

换根 \(dp\) 即可。

H [*hard]

瞅了眼题解,这个值域分块还挺妙的。

考虑值域分块,每一块的大小为 \(T\) 。对于每个值域块,处理出 \(\frac{T(T-1)}{2}\) 个区间的集合。接着询问的时候,将整块逐次合并即可,容易发现这里的合并次数是 \(\frac{n}{T}\),因此总共的区间数就是:

\[\frac{n(T-1)}{2}+\frac{qn}{T} \]

打表跑一跑发现 \(T=362\) 最优秀,这个看起来是 \(\sqrt{2q}\) 的样子。

接下来考虑上面那个考虑 "\(\frac{T(T-1)}{2}\) 个区间的集合" 怎么做。考虑按照值域分治,然后合并即可。计算这个需要的次数,容易发现:

\[S(n)=\frac{n}{2}\sum_{i=0}^{2^i\leq n}2^i-1 \]

容易发现后面那个东西一定小于 \(2n\),因此 \(S(n)\approx n^2\)
然后又跑一下,发现最小值是 \(T=256\),这个就是 \(\sqrt{q}\) 了。

Contest 1394 (Codeforces Round #664 (Div. 1)),[1800~3500]

C [*medium]

不难发现只需要使得 \(s\)\(t\) 中的 BN 出现次数分别一样即可。

可以想到将 \(s\) 化成一个点(坐标用 BN 的出现次数来表示),画图将这些操作表示出来,不难发现形成的是一个凸的图形。考虑二分走的步数,用代数式表示这个图形,这样就变成了对于每一个 \(s\) ,图形是否有交。判断一下即可。

D [*medium]

首先如果一条边的两个端点优先级不相等,那么这条边就已经有方向了。令点 \(u\) 的入边有 \(a\) 条,出边有 \(b\) 条,那么其贡献为 \(a_u\times \max(a,b)\)

对于没有方向的边,考虑 \(dp\) 。令 \(dp_{u,0/1}\) 表示对于 \(u\) 来说,父亲到其的边是入边/出边时,子树内的最小代价。转移的时候对于不能确定方向的边所连儿子全部都拿出来,然后钦定这些儿子全部选的入边,最后再逐次换成出边计算贡献即可,要求最优方案就将儿子按照 \(dp\) 状态的差排序即可。

Contest 1439 (Codeforces Round #684 (Div. 1)),[1500~3500]

B [*medium]

先将无用的度数小于 \(k-1\) 的点全部删去,这些点必然是没有贡献的。

然后就是删度数等于 \(k-1\) 的点,删的时候判断一下能不能团,显然团的边数是 \(\frac{k(k-1)}{2}\),因此 \(k\leq \sqrt{m}\) 时才有这个必要,显然又因为这样的点不超过 \(\frac{m}{k}\) 个,所以复杂度其实是 \(O(m\sqrt{m}\log m)\)\(\log\) 用来查边,实现优秀的话可以通过。

最后的话就是剩下的情况了。

C [*medium]

首先 \(1\) 操作不改变原序列的单调性。

接下来考虑 \(2\) 操作,容易发现一定是一段一段吃的,而且段数不超过 \(\log\) 。因为后一项不比前一项大,所以对于每一段,怎么吃都会将 \(y\) 至少吃掉一半,那么找段的话直接在线段树上二分即可。

posted @ 2021-01-29 23:40  Moonlightsqwq  阅读(184)  评论(0)    收藏  举报