寒假营4

D 数论,特判!!

每次可以把一个数减一,零一个数加一,问最终整个序列有多少种可能的gcd。一个数是整个数组的gcd,他首先一定是每个元素的约数,也就是说所有元素的和是这个数的倍数。

我们现在得到了一个判断的必要条件,但它实际上也是一个充分条件,可以通过构造证明:我们可以把一个数减一,零一个数加一,那么我们实际上就可以把sigma(a(i))的值随意分配到n个位置上。那么如果sigma(a(i))是一个数x的倍数,我们可以把一个元素变成x,剩下的元素都变成kx,k>=1。此时整个序列的gcd就是x了。

前面的比赛时都想到了,但是没考虑到元素个数为1的特判,我以为都说gcd了,肯定至少有两个数吧,结果不是,一个数的gcd是1……以后遇到给一个数组的问题,一定要考虑一下长度为1的情况。

using namespace std;
const int N=2e5+10;
signed main(){
    int n;
    cin>>n;
    if(n==1){//特判
        cout<<n;
        return 0;
    }
    long long sum=0;
    for(int i=1;i<=n;i++){
        int t;
        cin>>t;
        sum+=t;
    }
    int ans=0;
    for(int i=1;i<=sum/n;i++){
        if(sum%i==0&&sum/i>=n)ans++;
        //每个数都不小于gcd,因此gcd枚举范围不超过平均值
    }
    cout<<ans;
}

E前缀和贪心

赛时想复杂了,总觉得贪心会有漏的,结果就是个贪心,从左往右选,如果有合法区间就选上,最后选中区间数就是最大了。唯一一个比较巧妙的地方是,怎么判断有没有可以和当前点组成合法区间的左端点,但这实际上也是一种比较典的思路,把所有左端点存起来,然后每读一个端点,用前缀和或者其他区间查询,检查有没有合法左端点。由于合法区间不能重叠,每次配对后,都要把前面存的左端点都删掉。

using namespace std;
const int N=2e5+10;
int pre[N];
set<int>s;
int main(){
    int n,k;
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        int t;
        cin>>t;
        pre[i]=(pre[i-1]+t)%k;//预处理前缀和模k
    }
    s.insert(0);
    int ans=0;
    for(int i=1;i<=n;i++){
        if(s.count(pre[i])){//存在合法左端点
            s={pre[i]};//清空之前存的左端点
            ans++;//配对
        }
        else{
            s.insert(pre[i]);//不存在合法左端点
            //把当前端点也加入左端点集合
        }
    }
    cout<<ans;
}

F dp

每个数可以选或不选,选足六个可以计算一个得分,问得分之和最大值?

首先一个显然的转移是dp[i]=max(dp[i],dp[j]+maxscore(j,i))。这样枚举ij就已经n^2(1e6)了,那么求maxscore(j,i)就不能再是O(n)了。注意到我们枚举j时j是连续变化的,那么maxscore(j,i)和maxscore(j+1,i)可能是可以递推的,这样就可以实现常数复杂度计算了。

具体来说,我们可以维护选1-5个数的最大得分是多少,然后每次都用选5个数得最大的分,加上当前枚举的a(j)作为6个数,去更新dp,然后1-4的最大得分,加上a(j)推出2-5得最大得分,a(j)用来更新选一个的最大得分。计算分数得公式里出现了乘法,那么还需要维护最小值,因为负的最小值乘一个负数也可能是最大值。

using namespace std;
const int N=1e3+10;
int a[N],dp[N];
void fix1(set<int>&a){
    while(a.size()>1){
        a.erase(a.begin());
    }
}
void fix2(set<int>&a){
    while(a.size()>1){
        a.erase(--a.end());
    }
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){
        set<int>mn1,mn2,mn3,mn4,mn5;
        set<int>mx1,mx2,mx3,mx4,mx5;
        for(int j=i;j<=n;j++){
            for(auto &k:mx5){//更新dp
                dp[j]=max(dp[j],dp[i-1]+k-a[j]);
            }
            for(auto &k:mn5){
                dp[j]=max(dp[j],dp[i-1]+k-a[j]);
            } 
            for(auto &k:mn4){//更新5
                mn5.insert(k*a[j]);
                mx5.insert(k*a[j]);
            }
            for(auto &k:mx4){
                mn5.insert(k*a[j]);
                mx5.insert(k*a[j]);
            }
            for(auto &k:mx3){//更新4
                mn4.insert(k-a[j]);
                mx4.insert(k-a[j]);
            }
            for(auto &k:mn3){
                mn4.insert(k-a[j]);
                mx4.insert(k-a[j]);
            }
            for(auto &k:mn2){//更新3
                mn3.insert(k*a[j]);
                mx3.insert(k*a[j]);
            }
            for(auto &k:mx2){
                mn3.insert(k*a[j]);
                mx3.insert(k*a[j]);
            }
            for(auto &k:mx1){//更新2
                mn2.insert(k-a[j]);
                mx2.insert(k-a[j]);
            }
            for(auto &k:mn1){
                mn2.insert(k-a[j]);
                mx2.insert(k-a[j]);
            }
            mx1.insert(a[j]);//更新1
            mn1.insert(a[j]);
            fix1(mx1);//利用set得排序,把没用的都删掉,只留下最值
            fix1(mx2);
            fix1(mx3);
            fix1(mx4);
            fix1(mx5);
            fix2(mn1);
            fix2(mn2);
            fix2(mn3);
            fix2(mn4);
            fix2(mn5);
        }
    }
    cout<<*max_element(dp+1,dp+1+n);
}

G 暴力枚举

虽然一看数据就知道能暴力,但是暴力的写法也是很重要的,好的写法可以很快出,丑的写法可能虽然理论上能写,但实际写起来太麻烦导致写不出来。

一种简洁的写法是,枚举三角形顶点,一行行往下判断,直到越界,判断时就看两点之间是否全是星号。

using namespace std;
int n,m;
char a[550][550];
int pre[550][550];
int ans;
void check(int h,int l,int r){
    if(h>n||l<1||r>m)return;//越界返回
    if(pre[h][r]-pre[h][l-1]==0)ans++;//全是星号合法
    if(a[h][l]=='*'&&a[h][r]=='*')check(h+1,l-1,r+1);//看下一行,边往外扩
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
            pre[i][j]=pre[i][j-1]+(a[i][j]=='.');//预处理星号个数前缀和
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i][j]=='*')check(i+1,j-1,j+1);//枚举顶点
        }
    }
    cout<<ans;
}

H 树状数组统计合法对数

遍历所有点就已经1e6了,那么计算合法三角形就不能O(n)暴力了,必须logn。这个复杂度能想到的做法就只剩下树状数组维护合法端点数了。

这其实是一种很经典的思想,问有多少合法数对,暴力做法是n^2枚举所有组合,树状数组做法是,每读一个数,在权值树状数组上存起来,并且利用树状数组的区间查询,计算有多少可以和当前数配对的数。每个数都有一个能配对的范围,用这个范围决定查询区间。根据当前枚举位置,也可以把树状数组中那些作用范围到不了当前位置的数删掉。

具体来说,在这道题里我们要统计的点对就是三角形的顶点和底边中点(当然也可以选别的,比如右下角顶点),作用范围就是这个顶点/底边中点,最远可以和多远的点组成三角形,对底边中点来说就是左右两侧底边长度的最小值,对顶点来说就是左右延伸出的斜边长度的最小值。这些我们可以先预处理。

然后枚举每一列的所有点,每读一个星号,就在树状数组上插入这个点,然后统计这个点和已存在的点能组成的合法点对(三角形)数。横坐标每移动一次,就把失效的底边中点删掉。

#include<bits/stdc++.h>
using namespace std;
const int N=3e3+10;
char a[N][N];
int t[N],n,m,l[N][N],r[N][N],ul[N][N],ur[N][N];
int lowbit(int x){
    return x&(-x);
}
int sum(int x){
    int sum=0;
    while(x>=1){
        sum+=t[x];
        x-=lowbit(x);
    }
    return sum;
}
void add(int i,int x){
    while(i<=n){
        t[i]+=x;
        i+=lowbit(i);
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
        }
    }
    for(int i=n;i>=1;i--){
        for(int j=1;j<=m;j++){
            if(a[i][j]=='*'){
                l[i][j]=l[i][j-1]+1;//底边中点向左最大长度
                ul[i][j]=ul[i+1][j-1]+1;//向左下斜边最大长度
                ur[i][j]=ur[i+1][j+1]+1;//右下斜边最大长度
            }
            else{
                l[i][j]=ur[i][j]=ul[i][j]=0;
            }
        }
        for(int j=m;j>=1;j--){
            if(a[i][j]=='*'){
                r[i][j]=r[i][j+1]+1;//底边中点向右最大长度
            }
            else r[i][j]=0;
        }
    }
//     for(int i=1;i<=n;i++){
//         for(int j=1;j<=m;j++){
//             cout<<l[i][j]<<' ';
//         }
//         cout<<'\n';
//     }
    long long ans=0;
    for(int i=1;i<=m;i++){
        memset(t,0,sizeof(t));//树状数组清空
        vector<vector<int>>del(n+1);
        for(int j=n;j>=1;j--){//枚举这一列所有点
            if(a[j][i]=='*'){
                int delpos=j-min(l[j][i],r[j][i])+1;
                if(delpos>=1)del[delpos].push_back(j);//计算失效位置
                int pos=min(ur[j][i],ul[j][i])+j-1;//作为顶点的范围
                ans+=sum(pos);//统计范围内底边中点数
                add(j,1);//作为底边中点插入
            }
            for(auto &k:del[j]){//删除失效的底边中点
                add(k,-1);
            }
        }
    }
    cout<<ans;
}

J 计算几何 状压dp

其实没那么难,主要是读懂题意。每次画线都会把当前线上,以及当前线连着的线上所有点都染成一个新的颜色,问最少画几次可以把所有点都染成一个颜色。

观察数据范围,点数只有20个,显然状压。一个比较重要的观察是,每次都从已经染色的点上出发画新线,就是最优做法了,每次都挑未染色的点连线,最后再连起来,画线次数不会比第一种方式少。

那么状态转移就好想了,任何时刻已染色的点,颜色肯定都是一样的,那么枚举所有已染色的点,枚举该点引出的所有直线进行染色,染色后的状态就是染色前的状态并上新画直线对应状态,转移时取最小次数。

我们枚举时用到了所有直线对应的状态,这需要先预处理:两点确定直线枚举所有直线,然后再枚举所有点,判断是否在线上,在的话就把直线对应状态上加上这个点。dp初始化则是所有直线的画线次数为1。


using namespace std;
using i64 = long long;
using Point = complex<i64>;

auto cross(const Point &a, const Point &b) {//叉乘
    return imag(conj(a) * b);
}

bool onLine(Point &a, Point &b, Point &c) {
    return cross(b - a, c - a) == 0;//叉乘为0判共线
}
int d[30][30],dp[1<<21];
Point a[30];
int main(){
    int n;
    cin>>n;
    for(int i=0;i<(1<<n);i++)dp[i]=n;
    for(int i=0;i<n;i++){
        int x,y;
        cin>>x>>y;
        a[i]=Point(x,y);//存点   
    }
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){//枚举直线
            if(i!=j){
                for(int k=0;k<n;k++){//枚举点
                    if(onLine(a[k], a[j], a[i])){//判断在线上
                        d[i][j]|=1<<k;//状态加入该点
                    }
                }
            }
            else{
                d[i][j]|=1<<i;//只经过自己一个点的直线
            }
            dp[d[i][j]]=1;//所有直线染色次数初始化为1
        }
    }
    for(int i=1;i<(1<<n);i++){//枚举染色状态
        for(int j=0;j<n;j++){
            if(i>>j&1){//枚举状态上已染色点
                for(int k=0;k<n;k++){//枚举从该点引出的直线
                    dp[i|d[j][k]]=min(dp[i|d[j][k]],dp[i]+1);
                    //转移到染这条线的状态,点集取并
                }
            }
        }
    }
    cout<<dp[(1<<n)-1];//全染色的最小操作次数
}

K 线段树

一眼就能看出是线段树,但是有三种情况,区间合并的情况有点多,还是很考验码力的。

关键是tag的设计,复杂的合并往往需要很多的tag,该设tag的时候就设,反正tag变多也只是常数变大,但是tag维护的信息可以降低思考的复杂度。换句话说就是减低思维难度,增加码量,这其实是划算的,增加码量无非就是写的时候细心点,多考虑几种特殊情况,但是tag少了可能根本做不出来。

黄色当前堆加一,蓝色新开一堆加一,红色当前堆先翻倍再加一。可以发现有蓝色出现就会新开一堆,后面的操作对当前堆就没用了。那么合并的时候,右区间只有第一个蓝色之前的操作能作用在左区间最后一堆(最后一个蓝色之后)上,后面的操作都可以不管。操作也只有红黄两种,黄色其实没用,因为黄色对个数的贡献在前面就已经算过了,他不会在合并时影响到左区间,红色的个数则需要记录,每一个红色都会让左区间最后一堆翻倍,红色对右区间第一堆的影响其实也在前面合并的时候计算过了,不用管。

那么我们需要一个blue维护是否有蓝色,对于从左往右第一个蓝色之前,最后一个蓝色之后,和两个蓝色之间的数据,需要三个tag分别维护,每个tag都需要包含总个数和红色个数两个量。两个没有蓝色的区间相加,合并时计算规则和有蓝色的不一样,也开一个tag维护。

接下来就是复杂的分类讨论合并,见代码

using namespace std;
#define ll long long 
#define db double
#define inf 0x3f3f3f3f
#define rep(i,x,y) for(int i=(x);i<=(y);i++)
#define rep1(i,x,y) for(int i=(x);i>=(y);i--)
#define pii pair<int,int>
#define ls u<<1
#define rs u<<1|1
#define int ll
const int N=2e5+10;
const int M1=1e9+7,M2=998244353; 
ll power(ll a, ll n)//注意a如果为阶乘的话使用ll接收
{
    int ret = 1;
    while (n)
    {
        if (n & 1) ret = ret * a%M2;
        a = a * a%M2;
        n >>= 1;
    }
    return ret;
}
ll inv(ll x){
	return power(x, M2-2);
} 
int dx[4]={1,0,-1,0},dy[4]={0,-1,0,1};
int pow2[N];
string s;
struct Data{
	int a,b;//总个数和红色个数
};

//直接重载加号可以让线段树内部更简洁
Data operator+(const Data& a,const Data& b){
	Data ans={};
	ans.a=(a.a*pow2[b.b]+b.a)%M1;//右边的红色会让左边翻倍
	ans.b=a.b+b.b;//红色个数直接相加
	return ans;
}
struct Node{
    int l,r;
    ll blue,mid;
    Data data,ldata,rdata;//有蓝时,分为ldata,mid,rdata
    //没有蓝时就是data
};
Node operator+(const Node& a,const Node& b){
	Node ans={};
	ans.l=a.l;//边界修改
	ans.r=b.r;
	if(a.blue&&b.blue){//两边都有蓝
		auto t=a.rdata+b.ldata;
		ans.ldata=a.ldata;//左左成为左
		ans.rdata=b.rdata;//右右成为右
		ans.mid=(a.mid+b.mid+t.a)%M1;//左右,右左的和,左中加右中成为中
		ans.blue=1;
	}
	else if(a.blue&&!b.blue){//左边有蓝,右边没有
		ans.ldata=a.ldata;//左左成为左
		ans.mid=a.mid;//左中成为中
		ans.rdata=a.rdata+b.data;//左右加右成为右
		ans.blue=1;
	}
	else if(!a.blue&&b.blue){
		ans.ldata=a.data+b.ldata;
		ans.mid=b.mid;
		ans.rdata=b.rdata;
		ans.blue=1;
	}
	else{//左右都没蓝
		ans.data=a.data+b.data;//直接加
	}
	return ans;
}
struct Tree{
    Node tr[N<<2];
     
    void pushup(int u){
        tr[u]=tr[ls]+tr[rs];//重载了加号,区间和并就可以直接加
    }
     
//    void pushdown(int u){
//        if(tr[u].add){
//            tr[ls].mx+=tr[u].add;
//            tr[rs].mx+=tr[u].add;
//            tr[ls].add+=tr[u].add;
//            tr[rs].add+=tr[u].add;
//            tr[u].add=0;
//        }
//    }
     
    void build(int u,int l,int r){
        tr[u].l=l;
        tr[u].r=r;
        if(l==r){
            if(s[l]=='Y'){
				tr[u].data.a=1;	
			}
			else if(s[l]=='R'){
				tr[u].data.a=1;
				tr[u].data.b=1;
			}
			else{
                tr[u].rdata.a=1;
				tr[u].blue=1;
			}
            return;
        }
        int mid=(l+r)>>1;
        build(ls,l,mid);    build(rs,mid+1,r);
        pushup(u);
    }
     
    void modify(int u,int l,int r,char val){
        if(tr[u].l>=l&&tr[u].r<=r){   
        	s[l]=val;
        	Node ans={};
        	ans.l=l;
        	ans.r=r;
        	if(s[l]=='Y'){
				ans.data.a=1;	
			}
			else if(s[l]=='R'){
				ans.data.a=1;
				ans.data.b=1;
			}
			else{
                ans.rdata.a=1;
				ans.blue=1;
			}
			tr[u]=ans;
            return ;
        }
        else{
            int mid=(tr[u].l+tr[u].r)>>1;
//            pushdown(u);
            if(mid>=l)   modify(ls,l,r,val);
            if(r>mid) modify(rs,l,r,val);
            pushup(u);  
        }
    }
     
    Node query(int u,int l,int r){//合并要返回区间节点
        if(l<=tr[u].l&&tr[u].r<=r)    return  tr[u];
//        pushdown(u);
        int mid=(tr[u].l+tr[u].r)>>1;
        if(r<=mid)return query(ls,l,r);
        if(l>mid)return query(rs,l,r);
        return query(ls,l,r)+query(rs,l, r);
    }
}t;
void solve(void){
	int n,q;
	cin>>n>>q;
    pow2[0]=1;
    rep(i,1,n)pow2[i]=pow2[i-1]*2%M1;
	cin>>s;
	s="!"+s;
	t.build(1,1,n);
	while(q--){
		int op;
		cin>>op;
		if(op==1){
			int p;
			char c;
			cin>>p>>c;
			t.modify(1,p,p,c);
		}
		else{
			int l,r;
			cin>>l>>r;
			auto ans=t.query(1,l,r);
			cout<<(ans.ldata.a+ans.rdata.a+ans.mid+ans.data.a)%M1<<'\n';
            //所有个数都加起来,无意义的都是0,不影响
		}
	}
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int test=1;
//    cin>>test; 
    while(test--){
        solve();
    }
}