雅礼集训做题记录

感觉csp之前为了补基础写的题有些过于简单和套路了……
这些题大多是看了些别的题解后,自己思考再写出来的。

10.24

市场

考点:线段树。

做法:

其它三个操作都是线段树的常规操作。主要是除法下取整操作不太好处理。

肯定不能对区间和进行除法下取整,这样是一定会出错的。

这时候可以将除法转化为减法来考虑。

当一个区间最大值的减少量和区间最小值的减少量相同时,整个区间的减少量相同。

对于一个单调不下降序列,数列中的数都除一个数下取整时,减少量也是单调不下降的。(假设除数为\(k\),原序列中的数为\(x\),则减少量为\(x-\lfloor\frac{x}{k}\rfloor=\lceil (1-\frac{1}{k})x\rceil\)。当\(k\)固定时,\(x\)越大,后面的\(\lceil (1-\frac{1}{k})x\rceil\)一定是单调不降的。

那么最小减少量应该是在最小值处取得,最大减少量应该是在最大值处取得。

当最小减少量和最大减少量相等时,整个序列的减少量都相等。

其中区间最小值\最大值,区间和,区间加的复杂度都是\(O(qlogn)\),一个数最多被除以\(log_{a_i}\)次就不能再继续产生贡献了,最坏的情况就是每次都遍历\(n\)个区间,时间复杂度\(O(30\times n)\)

code
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
int const maxn=100101;
typedef long long LL;
int n,q;
LL a[maxn];
struct Node{
    LL mx,mn;
    LL sum;
    LL addtag;
}tree[maxn<<2];
void pushup(int rt){
    tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum;
    tree[rt].mn=min(tree[rt<<1].mn,tree[rt<<1|1].mn);
    tree[rt].mx=max(tree[rt<<1].mx,tree[rt<<1|1].mx);
}
void pushdown(int rt,int ln,int rn){
    if(tree[rt].addtag){
        tree[rt<<1].addtag+=tree[rt].addtag;
        tree[rt<<1|1].addtag+=tree[rt].addtag;
        tree[rt<<1].sum+=1ll*tree[rt].addtag*ln;
        tree[rt<<1|1].sum+=1ll*tree[rt].addtag*rn;
        tree[rt<<1].mn+=tree[rt].addtag;
        tree[rt<<1|1].mn+=tree[rt].addtag;
        tree[rt<<1].mx+=tree[rt].addtag;
        tree[rt<<1|1].mx+=tree[rt].addtag;
        tree[rt].addtag=0ll;
    }
}
void build(int l,int r,int rt){
    if(l==r){
        tree[rt].sum=a[l];
        tree[rt].mn=tree[rt].mx=a[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(l,mid,rt<<1);
    build(mid+1,r,rt<<1|1);
    pushup(rt);
}
//1:区间加 2:区间除 3:区间最小值 4:区间和
void add_update(int ql,int qr,LL val,int l,int r,int rt)
{
    if((ql<=l)&&(r<=qr)){
        tree[rt].addtag+=val;
        tree[rt].sum+=1ll*(r-l+1)*val;
        tree[rt].mn+=val;tree[rt].mx+=val;
        return ;
    }
    int mid=(l+r)>>1;
    pushdown(rt,mid-l+1,r-mid);
    if(ql<=mid) add_update(ql,qr,val,l,mid,rt<<1);
    if(qr>mid) add_update(ql,qr,val,mid+1,r,rt<<1|1);
    pushup(rt);
}
void div_update(int ql,int qr,LL val,int l,int r,int rt){
    if((ql<=l)&&(r<=qr)){
        LL premx=tree[rt].mx,aftmx=(LL)floor((double)tree[rt].mx/(double)val),difmx=premx-aftmx;
        LL premn=tree[rt].mn,aftmn=(LL)floor((double)tree[rt].mn/(double)val),difmn=premn-aftmn;
        if(difmx==difmn){
            tree[rt].sum-=1ll*(r-l+1)*difmx;
            tree[rt].mn-=difmx;
            tree[rt].mx-=difmx;
            tree[rt].addtag-=difmx;
            return ;
        }
    }
    int mid=(l+r)>>1;
    pushdown(rt,mid-l+1,r-mid);
    if(ql<=mid) div_update(ql,qr,val,l,mid,rt<<1);
    if(qr>mid) div_update(ql,qr,val,mid+1,r,rt<<1|1);
    pushup(rt);
}
LL qrymn(int ql,int qr,int l,int r,int rt){
    if((ql<=l)&&(r<=qr)){
        return tree[rt].mn;
    }
    int mid=(l+r)>>1;
    pushdown(rt,mid-l+1,r-mid);
    LL minn=(LL)2000000007;
    if(ql<=mid) minn=min(minn,qrymn(ql,qr,l,mid,rt<<1));
    if(qr>mid) minn=min(minn,qrymn(ql,qr,mid+1,r,rt<<1|1));
    return minn;
}
LL qrysum(int ql,int qr,int l,int r,int rt){
    if((ql<=l)&&(r<=qr)){
        return tree[rt].sum;
    }
    int mid=(l+r)>>1;
    pushdown(rt,mid-l+1,r-mid);
    LL summ=0ll;
    if(ql<=mid) summ+=qrysum(ql,qr,l,mid,rt<<1);
    if(qr>mid) summ+=qrysum(ql,qr,mid+1,r,rt<<1|1);
    return summ;
}
int main(){
    // freopen("a.in","r",stdin);
    // freopen("a.out","w",stdout);
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    build(1,n,1);
    for(int i=1;i<=q;i++){
        int opt;
        scanf("%d",&opt);
        if(opt==1){
            int l,r;
            LL c;
            scanf("%d%d%lld",&l,&r,&c);
            add_update(l+1,r+1,c,1,n,1);
        }
        if(opt==2){
            int l,r;
            LL d;
            scanf("%d%d%lld",&l,&r,&d);
            div_update(l+1,r+1,d,1,n,1);
        }
        if(opt==3){
            int l,r;
            scanf("%d%d",&l,&r);
            printf("%lld\n",qrymn(l+1,r+1,1,n,1));
        }
        if(opt==4){
            int l,r;
            scanf("%d%d",&l,&r);
            printf("%lld\n",qrysum(l+1,r+1,1,n,1));
        }
    }
    return 0;
}

10.25

矩阵

考点:构造

做法:

想不到任何算法的题,考虑想结论。

一定是拼凑出来一个全黑的行,然后用该行填满其它不是全黑的列。

没有办法用一个不是全黑的行将这个矩阵变为全黑的,因为每次用一行去填补一列时,一定会增加新的白格。

\(k\)行的黑格一定是由第\(k\)列上的黑格染的。

题目已给出。

加入说现在要将第\(i\)行染成全黑的,如果这一行上某一列的格子已经是黑色了,对答案没有贡献。

如果第\(k\)列上的格子不是黑色,找第\(i\)列上的格子。

如果第\(i\)列的格子不是全白,那么对答案的贡献就是1。

如果第\(i\)列的格子也是全白的,那么只需要在整个矩形中找到一个不是黑色的格子将第\(i\)列上的任何一白色格子染成黑色即可。

将第\(i\)行染成黑色的以后,只要判断有多少列不是全黑的即可。严格来说,应该判断将一行染完色之后有多少列不是全黑的,但是判断染之前的也可以。

用第\(i\)行将第\(j\)行格子染黑。

如果第\(i\)行原来是全黑,那么这时候会将一行全染黑和用被全染黑的一行去染黑其它列会算重。但是会在将第\(i\)行作为被全染黑的一行时,将答案更新掉。

如果第\(i\)行不是全黑,那么将一行全染黑时,是全黑的列不会被重新染色,(第\(j\)个格子已经是黑色)不是全黑的列(第\(j\)个格子不是黑色)会被染色,同时必然会引入新的白格子。用被全染黑的一行染黑其它列时,同样全黑的列不会被重新染色,不是全黑的列,染完之后仍然不是全黑,需要被重新染色。

code
#include<algorithm>
#include<cstdio>
#include<iostream>
using namespace std;
int const maxn=1011;
using ll=long long;
inline ll read(){
    ll x=0;int c=getchar();
    while(c<'0'||c>'9') c=getchar();
    while('0'<=c&&c<='9'){x=10*x+c-'0';c=getchar();}
    return x;
}
int n,map[maxn][maxn];
string s;
bool flg;
//# 黑色 1 .白色 0
bool hasblack[maxn],haswhite[maxn];
int ans=0x7f7f7f7f;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>s;s=' '+s;
        for(int j=1;j<=n;j++){
            if(s[j]=='#'){
                flg=true;
                hasblack[j]=true;
                map[i][j]=1;
            }
            if(s[j]=='.'){
                haswhite[j]=true;
                map[i][j]=0;
            }
        }
    }
    if(!flg){
        printf("-1");
        return 0;
    }
    for(int i=1;i<=n;i++){
        int nowans=0;
        for(int j=1;j<=n;j++){
            if(map[i][j]==1) continue;
            if(map[i][j]==0){
                ++nowans;
            }
        }
        if(!hasblack[i]) ++nowans;
        for(int j=1;j<=n;j++){
            if(haswhite[j])
                ++nowans;
        }
        ans=min(ans,nowans);
    }
    printf("%d",ans);
    return 0;
}

字符串

考点:根号分治,SAM。

已经不会SAM了,希望有一天会了可以回来做。


10.27

水箱

考点:笛卡尔树,树上倍增,树形dp。

啃了很久才啃懂的一道题(因为太菜啦(>﹏<)

考虑水一点一点往上涨的。

对于一个隔板来说,当隔板两边水慢慢涨高,直至超过当前隔板的高度时,那么隔板两边水就连通了,此时隔板两边的左右区间相当于合并成了一个区间。

对于所有的隔板来说也是一样的情况。随着水慢慢上涨,高度较低的隔板的左右区间会先合并为一个区间,然后高度次低的隔板的左右区间合并为一个,……,最后高度最高的隔板的左右两个区间合并为一个区间。此时,最初所有被隔板分割的区间都合并为了一个区间。

image

所以可以考虑用隔板的高度区分不同的阶段,来判断不同阶段最多能满足多少条件。

记隔板左右两边水位高度为\(w\),当前隔板高度为\(h\),第一个比它高的隔板的高度为\(h'\)

当水的高度没有超过当前隔板的高度(即\(w\le h\)时):

可以满足的条件为当前隔板左右两个区间中水的高度任意(不超过隔板高度即可)以及\(h\)\(h'\)中没水的条件。

\(x\)为当前隔板的左右区间编号。

\(y<h\),且对应的\(k\)\(1\)的条件。

\(h\le y< h'\),且对应的\(k\)\(0\)的条件。

当水的高度超过当前隔板的高度(即\(w> h\)时):

可以满足的条件为\(h\)\(w\)有水的条件以及\(w\)\(h'\)中没水的条件。

\(x\)为当前隔板的左右区间编号。

\(h\le y\le w\),且对应的\(k\)\(1\)的条件。

\(w<y< h'\),且对应的\(k\)\(0\)的条件。

其中\(h\le w< h'\)

要求的是对于每个阶段水高度超过/没超过当前隔板高度可以满足条件的数量的最大值。可以用dp求出。

怎么合并这些阶段呢?

根据前面分析,隔板高度越低的左右两个区间会先合并。合并的顺序是隔板高度由低到高。那么需要对隔板高度建一棵类似于大根堆的结构,然后从堆底合并到堆顶。

同时还需要将隔板左右的两个区间内的条件对应到隔板上去,也就是需要保留隔板的位置信息。

发现笛卡尔树可以很好的保留这两种性质。

所以可以建一棵笛卡尔树,在笛卡尔树上进行树形dp。(图片是嫖的)

image

一些实现的细节

怎么将左右两个区间对应到隔板所在的笛卡尔树的节点上去呢?

只用将位于笛卡尔树叶子结点的隔板的左右区间对应上去即可。

因为非叶子结点的区间一定是由最开始被隔板隔开的左右两个区间合并成的一个区间一层层合并上去的。

由于笛卡尔树的中序遍历就是原序列,对笛卡尔树进行dfs中序遍历即可。

怎么知道条件在笛卡尔树中哪一个点的管辖范围里呢?

可以从条件所在区间对应的笛卡尔树上的节点在树上倍增,找到最后一个高度\(\le\)条件的点。

code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
int const maxn=200101;
int const Log=20;
int T;
int n,m,id,hig,has;
int h[maxn];
int stk[maxn],top;
int ls[maxn],rs[maxn];
int tot,pos[maxn],f[maxn][21];
vector< pair<int,int> > v[maxn];
int dp[maxn<<1][2];
void build(int &x,int fa){
    if(!x){
        ++tot;
        pos[tot]=x=n-1+tot;
    }
    f[x][0]=fa;
    for(int i=1;i<=Log;i++)
        f[x][i]=f[f[x][i-1]][i-1];
    if(x<n){
        build(ls[x],x);build(rs[x],x);
    }
    return;
}
void dfs(int x){
    if(!x)return ;
    dfs(ls[x]);dfs(rs[x]);
    sort(v[x].begin(),v[x].end());
    int sum0=0,sum1=0;
    for(unsigned int i=0;i<v[x].size();i++)
        if(!v[x][i].second)
            ++sum0;
    dp[x][0]=dp[ls[x]][0]+dp[rs[x]][0]+sum0;
    for(unsigned int i=0;i<v[x].size();i++){
        v[x][i].second?sum1++:sum0--;
        dp[x][0]=max(dp[x][0],dp[ls[x]][1]+dp[rs[x]][1]+sum1+sum0);
    }
    dp[x][1]=dp[ls[x]][1]+dp[rs[x]][1]+sum1;
}
void init(){
    top=0;tot=0;
    for(int i=1;i<=2*n;i++){
        h[i]=0;
        stk[i]=0;
        ls[i]=rs[i]=0;
        pos[i]=0;
        v[i].clear();
        dp[i][0]=0;dp[i][1]=0;
        for(int j=0;j<=20;j++)
            f[i][j]=0;
    }  
}
int main(){
    // freopen("a.in","r",stdin);
    // freopen("a.out","w",stdout);
    scanf("%d",&T);
    while(T--){
        top=0;tot=0;
        for(int i=1;i<=n;i++){

        }
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n-1;i++){
            scanf("%d",&h[i]);
            while(top&&h[stk[top]]<=h[i]) ls[i]=stk[top--];
            if(top) rs[stk[top]]=i;
            stk[++top]=i;
        }
        build(stk[1],0);
        while(m--){
            scanf("%d%d%d",&id,&hig,&has);id=pos[id];
            for(int i=Log;i>=0;i--)
                if((f[id][i])&&(h[f[id][i]]<=hig))
                    id=f[id][i];
            v[id].push_back(make_pair(hig,has));
        }
        dfs(stk[1]);
        printf("%d\n",max(dp[stk[1]][0],dp[stk[1]][1]));
        init();
    }
    return 0;
}

10.28

棋盘游戏

考点:二分图博弈。

待补坑。

线段游戏

考点:李超线段树

洗衣服

标签:

贪心,堆。

做法:

如果只有洗或者只有烘干,那么就是一个简单的贪心了。按时间从小到大选就可以 了。

现在洗和烘干都有,而且要求洗必须在烘干之前。

可以先分别考虑洗和烘干,再将两者结合在一起。

求出洗完\(i\)件衣服的最短时间和烘干完\(i\)件衣服的最短时间。

随着\(i\)增大,时间也一定是递增的。

然后让洗完\(1\)件衣服匹配烘干完\(l\)件衣服的时间,洗完\(2\)件匹配烘干\(l-1\)件衣服的时间,……,洗完\(i\)件衣服匹配烘干\(l-i+1\)件衣服的时间。

怎么保证先洗后烘?

让洗的时间加上烘干的时间就保证了。

为什么是最小的时间?

根据排序不等式,两个单调上升的序列,倒序和<乱序和<正序和。

具体到这道题上来说,若要使用时最长的一件衣服的用时最短。一定是先洗完的衣服匹配一个较长的烘干时间。如果不用它匹配,而换用一个洗完时间更长的衣服匹配较长的烘干时间,那么一定会拉长用时最长的时间。

实现:

开两个优先队列模拟就可以了。

code

做题抓重点信息。

有多个限制的贪心条件,可以先考虑把它们拆开,先贪心的满足其中一个,在使它们组合起来的花费最小。

编码

考点:trie,2-SAT

会了2-SAT再回来补题QwQ

10.30

珠宝

标签:

同余分类,决策单调性优化dp

做法:

一个很显然的暴力思路是对于每一个\(i\)是跑一遍01背包,其中\(c\)相当于体积,\(v\)相当于价值。这样的复杂度应该是\(\sum\limits_{i=1}^k n\times i\)。不能接受。观察到\(c\)很小,所以肯定

注意到本题的\(c\)很小,考虑定义一个和\(c\)有关的状态。

\(f(x,j)\)表示考虑到价格为\(x\)的物品,一共花费了\(j\)元的最大收益。

将价格为\(x\)的物品按照收益从大到小排序,记这个数组为\(w\),不难发现选的一定是\(w\)的一段前缀形式。

将所有\(j\)按照模\(x\)的余数分类,可以得到\(f(x,i)=\max\limits_{j\%x=i\%x}{f(x-1,j)+w(\frac{i-j}{x})}\)

同类的位置决策单调。

因为\(w\)是一个关于两个位置的上凸函数。

因为,对于同类的\(w\)数组,已经进行了排序。将收益大的物品放在了前面, 收益较小的物品放在了后面。也就是说越到后面,花费同样的\(x\)元时,可以收获的价值越小。反映到\(w\)函数上来说就是,横坐标增加同样的\(x\),纵坐标上升的越少。

证明单调性:

假定花了\(i\)元时的决策点为\(g[i]\)

那么对于任意的\(g[i]-x\)

\(f(x,g[i]-x)+w(\frac{i-g[i]+x}{x})<f(x,g[i])+w(\frac{i-g[i]}{x})\)

因为\(i-g[i]+x>i-g[i]\)

所以\(w(\frac{i+1-g[i]+x}{x})-w(\frac{i-g[i]+x}{x})<w(\frac{i+1-g[i]}{x})-w(\frac{i-g[i]}{x})\)

(前面已经提到\(w\)是一个上凸函数。越到后面,增加相同的量,函数上升的越少。)

\(i\)增大到\(i+1\)时:

\(f(x,g[i]-x)+w(\frac{i+1-g[i]+x}{x})<f(x,g[i])+w(\frac{i+1-g[i]}{x})\)

\(i\)增大到\(i+1\)时,\(g[i]-x\)还是不如\(g[i]\)优,\(g[i]-x\)不会成为\(i+1\)的决策点。

也就是说随着\(i\)增大,决策点一定也是不断增大的,它是具有单调性的。

处理方法:分治

code:

11.1

跳蚤王国的宰相

标签:

树的重心。

做法:

树的重心的两个性质:

树的重心到树上各点的距离和最短。

以这个节点为根,它的每个子树的大小,都不会超过整个树的大小的一半。

如果当前被钦定的节点是树的重心,那么需要消失\(0\)条边。

如果当前被钦定的节点不是重心,那么对于这个节点来说,超过整棵树大小一半的那棵子树,一定是树的重心所在的那一棵子树。

反证法:

若重心不在大小超过整棵子树大小一半的那棵子树中,而在一棵别的子树中。那么那棵子树大小超过一半的子树也会成为重心的子树。这就不符合重心的性质了。

可以考虑把重心的子树割一部分给这个节点。由于要割最少的边,考虑将重心的所有子树按照从大到小排序,依次割出去一棵重心的子树(此子树的大小必然也小于整棵树的一半),直至当前重心所在的子树大小小于整棵树的一半。

code:

有些细节还没想清楚。

目前为止还没有写出来

11.2

蛐蛐国的修墙方案

标签:

构造,搜索。

做法:

完全模拟题意,写一个\(n*2^n\)级别的玄学爆搜,可以过掉10%的数据。

如果将\(i\rightarrow p_i\)看成一条边,那么既然它保证有解就一定不会出现链或环加链的情况。

如果是链,那么\(n\)个点的链肯定没有\(n\)条边,不符合题意。

如果有了环+链,说明有一个点向多个点连边。说明有多个\(i\)\(p_i\)是一样的。\(p\)就不是一个\(n\)个数的排列了。

那么这\(n\)条边形成的一定是一个环或多个环。且每个环一个是一个偶环。

考虑在环中选边。由于要求每个点的出入度都是\(1\),那么一定是选了这条边,下一条边不选,再下一条边选……是一个奇环,一定会选两条相邻的边,不能构造出满足条件的序列。

不难发现,由于每个点出入度都为\(1\)的限定条件,当确定了环中的一条边选或不选时,就确定了环中所有的边选或不选。

所以只要在环中任选一条边确定状态就可以了。

而对于长度为\(2\)的环进行特判。钦定前面一个位置为左括号,后面一个位置为右括号(贪心地?)。

那么一个环的长度至少为\(4\),也就是至多有\(25\)个环。

对于每个环上任选一条边进行搜索,复杂度最差为\(O(2^{25})\)

标签:

dfs序,分块。

做法:

看到树,可以比较套路的想到利用它的dfs序转化为序列上的问题。

决斗

所有的侏儒坐成了一个环,但是环的情况不太好模拟,考虑把它断成一条链。

要把环断成链,且对正确性没有影响,那么一定是没有精灵穿过断点从一段去往另一段。

考虑什么样的位置满足这一条件?

\(sum_i\)为前\(i\)个侏儒被\(sum_i\)个精灵匹配。根据侏儒总数和精灵总数相等,\(sum_k-k=0\)

如果\(sum_i-i< 0\),即匹配前\(i\)个侏儒的总精灵数小于当前侏儒数,那么就一定会有后面的精灵跨过第\(i\)个位置来填补前面的侏儒,而前面的精灵不会跨过第\(i\)个位置去找后面的侏儒。那么就找到\(sum_i-i\)最小的一个位置,此时\(i\)\(i\)前面位置的精灵都不会去找后面的侏儒,而根据\(sum_k-k=0\)\(i\)后面位置的精灵一定都需要找前面的侏儒,从第\(i\)个位置断开即可。

数列

想了半天怎么转化每次只在最前面和最后面放这个条件。其实是从每一个点开始的最长上升子序列和最长下降子序列拼起来的长度的最大值。

把以\(i\)开头的最长下降子序列变成以\(i\)结尾的最长上升子序列,只要每次遇到最长下降子序列中的数都把它放到序列最左边即可。

其中既不在上升序列也不在下降序列中的数放哪里都无所谓啊。

难点是方案数……

之前做的这种题都是边求最大长度边转移方案,这道题显然不能这么搞。

posted @ 2021-10-28 18:37  RapunzelOnly  阅读(86)  评论(0)    收藏  举报