24.8.4(主席树,递推求组合数,警钟长鸣)

星期一:

洛谷 P3834                                                     洛谷传送门

思路:主席树板子题,数据需先离散化处理

代码如下:

const int N=2e5+10,M=210;
ll n;
int a[N];
struct seg_Tree{
#define lc(x) t[x].ch[0]
#define rc(x) t[x].ch[1]
	struct nod{
		int ch[2];
		int s;
	}t[N*20];
	int root[N],tot;
	void insert(int x,int &y,int l,int r,int v){
		y=++tot; t[y]=t[x],t[y].s++;
		if(l==r) return ;
		int mid=l+r>>1;
		if(v<=mid) insert(lc(x),lc(y),l,mid,v);
		else insert(rc(x),rc(y),mid+1,r,v);
	}
	int query(int x,int y,int l,int r,int k){
		if(l==r) return l;
		int mid=l+r>>1;
		int s=t[lc(y)].s-t[lc(x)].s;
		if(s>=k) return query(lc(x),lc(y),l,mid,k);
		else return query(rc(x),rc(y),mid+1,r,k-s);
	}
}tr;
void solve(){
	int m; cin >> n >> m;
	map<int,int>to,ori;
	for(int i=1;i<=n;i++){
		cin >> a[i];
		to[a[i]]=1;
	}
	int cnt=0;
	for(auto &[x,y]:to) y=++cnt,ori[cnt]=x;       //离散化处理
	for(int i=1;i<=n;i++)
		tr.insert(tr.root[i-1],tr.root[i],1,cnt,to[a[i]]);
	while(m--){
		int l,r,k; cin >> l >> r >> k;
		cout << ori[tr.query(tr.root[l-1],tr.root[r],1,cnt,k)] << "\n";
	}
}

板刷数据结构 F                                               牛客传送门

思路:这里用的是主席树写法,mex取值在 1-n+1间,若存在数大于n,则mex<=n,所以将大于n的数记为n+1即可

每个节点存了在此值域内的数的数量 s,和此节点值最后一次出现的下标 ed,若节点代表区间,则取 min,也就是所有数中最后出现下标最早的那个,这样即可在 root【r】为根的线段树中,    二分求出 mex,即最小的 ed<l 的数

在知道mex后,我们要知道的是大于mex的数有多少,那么可以求出 1-mex的数有多少,再用长度减去即可得到答案,求范围内数的数量则更是主席树的基操了

代码如下:

const int N=3e5+10,M=210;
ll n;
int a[N];
struct seg_Tree{
#define lc(x) t[x].ch[0]
#define rc(x) t[x].ch[1]
	struct nod{
		int ch[2];
		int s;
		int ed;
	}t[N*22];
	int root[N],tot;
	void insert(int x,int &y,int l,int r,int v,int p){
		y=++tot; t[y]=t[x],t[y].s++;
		if(l==r){
			t[y].ed=p;
			return ;
		}
		int mid=l+r>>1;
		if(v<=mid) insert(lc(x),lc(y),l,mid,v,p);
		else insert(rc(x),rc(y),mid+1,r,v,p);
		t[y].ed=min(t[lc(y)].ed,t[rc(y)].ed);
	}
	int query_mex(int y,int l,int r,int ql){
		if(l==r) return l;
		int mid=l+r>>1;
		if(t[lc(y)].ed<ql) return query_mex(lc(y),l,mid,ql);
		else return query_mex(rc(y),mid+1,r,ql);
	}
	int query_sum(int x,int y,int l,int r,int ql,int qr){
		if(ql<=l && qr>=r) return t[y].s-t[x].s;
		int mid=l+r>>1;
		if(ql>mid) return query_sum(rc(x),rc(y),mid+1,r,ql,qr);
		if(qr<=mid) return query_sum(lc(x),lc(y),l,mid,ql,qr);
		int res=0;
		res+=query_sum(rc(x),rc(y),mid+1,r,ql,qr);
		res+=query_sum(lc(x),lc(y),l,mid,ql,qr);
		return res;
	}
}tr;
void solve(){
	cin >> n;
	for(int i=1;i<=n;i++){
		cin >> a[i];
		if(a[i]>n) a[i]=n+1;
		tr.insert(tr.root[i-1],tr.root[i],1,n+1,a[i],i);
	}
	int q; cin >> q;
	while(q--){
		int l,r; cin >> l >> r;
		int mex=tr.query_mex(tr.root[r],1,n+1,l);
		int sum=tr.query_sum(tr.root[l-1],tr.root[r],1,n+1,1,mex);
		cout << r-l+1-sum << "\n";
	}
}

比上题更简单一点的洛谷P4137                   洛谷传送门

思路:属于上题的子问题,因为没有要求区间内大于mex的数有多少,所以可用普通的权值线段树实现,ed维护的信息与上题相同

将询问按右端点从小到大排序,一个一个地添加 a【i】,当 i等于询问右端点时,即可用与上题类似的二分求出mex

做了这题后更加理解了主席树的作用,若用普通的权值线段树,则无法实现查询区间大于mex的数的多少,因为不知道第 l次到 r次插入的是哪些数,只能查询从 1开始到 r插入的所有数,主席树的功能就在于能回溯到第 l-1次插入

代码如下:

const int N=3e5+10,M=210;
ll n;
int a[N];
struct seg_Tree{
#define lc p<<1
#define rc p<<1|1
	struct nod{
		int l,r;
		int ed;
	}t[N<<2];
	ll ql,qr,qv;
	nod merge(nod a,nod b){
		nod res;
		res.l=a.l,res.r=b.r;
		res.ed=min(a.ed,b.ed);
		return res;
	}
	void pushup(int p){t[p]=merge(t[lc],t[rc]);}
	void bd(int p,int l,int r){
		t[p]={l,r,0};
		if(l==r) return ;
		int mid=l+r>>1;
		bd(lc,l,mid);
		bd(rc,mid+1,r);
	}
	void update(int p){
		if(ql<=t[p].l && qr>=t[p].r){
			t[p].ed=qv;
			return ;
		}
		int mid=t[p].l+t[p].r>>1;
		if(ql<=mid) update(lc);
		if(qr>mid) update(rc);
		pushup(p);
	}
	void updt(int l,int r,int v){
		ql=l,qr=r;
		qv=v;
		update(1);
	}
	int fnd(int p,int l){
		if(t[p].l==t[p].r) return t[p].l;
		if(t[lc].ed<l) return fnd(lc,l);
		else return fnd(rc,l);
	}
}tr;
struct query{
	int l,r;
	int id,mex;
}qu[N];
void solve(){
	int q; cin >> n >> q;
	for(int i=1;i<=n;i++){
		cin >> a[i];
		a[i]++;
		if(a[i]>n) a[i]=n+1;
	}
	tr.bd(1,1,n+1);
	for(int i=1;i<=q;i++){
		cin >> qu[i].l >> qu[i].r;
		qu[i].id=i;
	}
	sort(qu+1,qu+q+1,[](query a,query b){
		return a.r<b.r;
	});
	int qid=1;
	for(int i=1;i<=n;i++){
		tr.updt(a[i],a[i],i);
		while(i==qu[qid].r){
			qu[qid].mex=tr.fnd(1,qu[qid].l);
			qid++;
		}
	}
	sort(qu+1,qu+q+1,[](query a,query b){
		return a.id<b.id;
	});
	for(int i=1;i<=q;i++) cout << qu[i].mex-1 << "\n";
}

复习下状压dp 炮兵阵地                                    牛客传送门

思路:dp[ i ][ a ][ b ]表示考虑到第 i行, i 行和 i-1行的状态为 a和 b的最多摆放数

判断状态是否合法,需要考虑 ph的限制,和上一行以及上上一行有无冲突

这里犯了个很蠢的错误判断,在dp转移时没取max,以为一个状态只会被赋值一次......害的de了半天

代码如下:

ll n;
int ma[1<<10],cnt;
int ph[1<<10];
int dp[110][1<<10][1<<10];
void solve(){
	int m; cin >> n >> m;
	for(int i=1;i<=n;i++){
		int mask=0;
		for(int j=1;j<=m;j++){
			char c; cin >> c;
			if(c=='H') mask+=1<<j-1;
		}
		ph[i]=mask;
	}
	for(int mask=0;mask<1<<m;mask++){
		if(mask&mask<<1 || mask&mask<<2) continue;
		ma[cnt++]=mask;
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		for(int a=0;a<cnt;a++){
			for(int b=0;b<cnt;b++){
				for(int c=0;c<cnt;c++){
					if(ma[a]&ph[i]) continue;
					if(ma[a]&ma[b] || ma[a]&ma[c]) continue;
					//dp[i][a][b]=dp[i-1][b][c]+__builtin_popcount(ma[a]);
					dp[i][a][b]=max(dp[i-1][b][c]+__builtin_popcount(ma[a]),dp[i][a][b]);
					ans=max(dp[i][a][b],ans);
				}
			}
		}
	}
	cout << ans << "\n";
}

星期二:

补 24牛客多校1 A                                              牛客传送门

题意:求序列数,满足长度为 n,元素非负且小于 2^m,且存在非空子序列按位与结果为1

思路:在序列中选中了 k个数作为子序列,那么这 k个数必须满足二进制最后一位为1,其余位按位与结果为0,把每一个数看作长为 m的二进制表达,那么 k个数就相当于长 m宽 k的矩阵

如下图:(搬运自2024牛客多校第一场A Bit Common & A Bit More Common - AcidBarium - 博客园 (cnblogs.com)

每一列有 2^k种可能,减去全1,即为2^k - 1,m-1列则共有 ( 2^k -1)^(m-1)种情况,未在子序列中的数则需满足最后一位为0,其余位任意,每列情况有 2^(n-k)个,有 m-1列,共 2^( (n-k)*(m-1) )种情况,再乘上 n个数中选 k个数的情况数即 C n,k

注意此题 mod由题目给出,不一定是质数,所以不能使用费马小定理计算组合数,需递推求值

代码如下:

ll n;
int q;
ll c[5050][5050];
ll qpow(ll a,ll n){
	ll res=1;
	while(n){
		if(n&1) (res*=a)%=q;
		(a*=a)%=q;
		n>>=1;
	}
	return res;
}
//ll c(ll n,ll m){
//	ll fz=1,fm=1;
//	for(int i=1;i<=m;i++) (fm*=i)%=q;
//	for(int i=n-m+1;i<=n;i++) (fz*=i)%=q;
//	return fz*qpow(fm,q-2)%q;
//}
void solve(){
	int m; cin >> n >> m >> q;
	for(int i=0;i<=n;i++){
		for(int j=0;j<=i;j++) c[i][j]=!j?1:(c[i-1][j]+c[i-1][j-1])%q;
	}                               // n^2提前处理组合数
	ll ans=0;
	for(int k=1;k<=n;k++){
		ans+=c[n][k]*qpow(qpow(2,k)-1,m-1)%q*qpow(2,(n-k)*(m-1)%q)%q;
		ans%=q;
	}
	cout << ans;
}

下午打了24牛客多校 5,还算不错,用偏门解法过了两道题

贴 24牛客多校5 入                                              牛客传送门

题意:给一无向图,路径规则为,从当前点前往邻点中权值最小的点,且其权值小于当前点,若无权值更小邻点则结束,现可任意安排权值和起点,问路径最长经过多少点

思路:暴搜+计数器优化,dfs规则可视为从 u点到 v点后,将 v点及u的所有邻点都标记上,不再到达,再加个 cnt防止TLE,第一次开2e7就过了,实测1e6也能过

代码如下:

ll n;
vector<int>ve[44];
int vi[44];
int ans;
int cnt=2e7;
void dfs(int x,int len){
    ans=max(len,ans);
    if(cnt--<0) return ;       //计数优化
    vector<int>to;
    for(int v:ve[x]) if(!vi[v]) to.push_back(v),vi[v]=1;  //先存入所有可达点
    for(int v:to){
        dfs(v,len+1);        //挨个dfs
        if(cnt--<0) break;
    }
    for(int v:to) vi[v]=0;
}
void solve(){
    int m; cin >> n >> m;
    for(int i=1;i<=m;i++){
        int u,v; cin >> u >> v;
        ve[u].push_back(v);
        ve[v].push_back(u);
    }
    for(int i=1;i<=n;i++){
        memset(vi,0,sizeof vi);
        vi[i]=1;
        dfs(i,1);
        if(cnt--<0) break;
    }
    cout << ans;
}

贴多校5 知                                                      牛客传送门

题意:有 n个关卡,每个关卡积分为 ai,可执行任意次操作为 使ai ++,ai+1 --,使 a乘积最大

思路:由乘积最大化易想到是让数组值尽量平均,问题是怎么操作呢?贪心很不好处理,并且因为乘的过程中要取模,答案大小会发生变化,所以很多做法也无法实现。

那么一次贪心不好贪,多贪几次呢,贪个几百次能贪出正确答案吗?在暴力之前,最重要的不是考虑时间复杂度,而是要确保其正确性。能想到如果 ai大于ai-1,那么转移一个1,一定不会使答案更劣,那么直接扫个几百几千次,就能得到答案了。这里第一次保守了只扫了100次wa了,300次能过,实测3000次也不会TLE,因为这个转移次数实际上是特别有限的

代码如下:

const int mod=998244353;
ll n;
int a[110];
void solve(){
    cin >> n;
    for(int i=1;i<=n;i++) cin >> a[i];
    for(int ii=1;ii<=300;ii++){
        for(int i=n;i>1;i--)
            while(a[i]>a[i-1]) a[i]--,a[i-1]++;
    }
    ll ans=1;
    for(int i=1;i<=n;i++) (ans*=a[i])%=mod;
    cout << ans << "\n";
}

补 24江苏省赛 E                                                  cf传送门

5月份做过这题,当时没学过主席树,用线段树瞎搞了下

题意:给一数组,有一操作为使区间最大值除2,q次询问,给出l r区间,问进行 k次操作后最大值

思路:每次插入 ai时,不断将其除2并继续插入,例如插入9,那么主席树中就插入9,4,2,1。询问就等价于找到区间内第 k+1大的值。

开始尝试对于一个下标 i的多次插入,都在 root【i】上完成,但de了很久后发现,插入若不新开一条链,可能会出现结构上的问题,于是作罢

需要注意,对于每个 i,既然有多次插入,就要将 i与实际插入的下标对应上,mp【i】的pair值对应了主席树上的下标区间

代码如下:

const int N=2e6+10,M=210;
ll n;
const int MAXN=1e5+10;
struct seg_Tree{
#define lc(x) t[x].ch[0]
#define rc(x) t[x].ch[1]
	struct nod{
		int ch[2];
		int s;
	}t[N*20];
	int root[N],tot;
	void insert(int x,int &y,int l,int r,int v){
		y=++tot; t[y]=t[x]; t[y].s++;
		if(l==r) return ;
		int mid=l+r>>1;
		if(v<=mid) insert(lc(x),lc(y),l,mid,v);
		else insert(rc(x),rc(y),mid+1,r,v);
	}
	int query(int x,int y,int l,int r,int k){        //二分搜索第k小值
		if(t[y].s-t[x].s<k) return 0;
		if(l==r) return l;
		int mid=l+r>>1;
		int sum=t[rc(y)].s-t[rc(x)].s;
		if(sum>=k) return query(rc(x),rc(y),mid+1,r,k);
		else return query(lc(x),lc(y),l,mid,k-sum);
	}
}tr;
void solve(){
	int q; cin >> n >> q;
	map<int,PII>mp; int idx=0;
	for(int i=1;i<=n;i++){
		int x; cin >> x;
		if(x) mp[i].first=idx+1;
		else mp[i].first=idx;
		while(x){
			tr.insert(tr.root[idx],tr.root[idx+1],0,MAXN,x),idx++;
			x>>=1;
		}
		mp[i].second=idx;
	}
	while(q--){
		int l,r,k; cin >> l >> r >> k; k++;;
		cout << tr.query(tr.root[mp[l].first-1],tr.root[mp[r].second],0,MAXN,k) << "\n";
	}
}

星期三:

下午 24河南萌新联赛 三,还行

贴 F                                                              牛客传送门

思路:很典的按位计算贡献,低位全0的情况下第一位贡献是 y,第二位是 y/2,第三位 y/4,但这肯定不是实际情况,我们需要计算从低位存在 1到低位全0需要累加多少次,然后才可以整除

代码如下:

void solve(){
	int x,y; cin >> x >> y;
	ll ans=0,sum=0;
	string s;
	for(int i=0;i<30;i++){
		ll nd=(1<<i)-sum;
		if(y<nd) break;         //从此位开始的更高位不会改变
		ans+=(y-nd)/(1<<i)+1;
		if(x&1<<i) sum+=1<<i;   //记录低位和
	}
	cout << ans << "\n";
}

贴 G                                                              牛客传送门

思路:正解 枚举+三分,赛时 暴力n^2+计数优化冲过去了,wa了23发

代码如下:

ll n;
int cnt;
void solve(){
	ll a,b,c,w; cin >> a >> b >> c >> n >> w;
	if(n<1000) cnt=2e8;
	else if(n<10000) cnt=1e6;
	else if(n<100000) cnt=3e6;
	else cnt=1e7;                         //根据n的大小分配cnt
	if(a<b) swap(a,b);
	if(a<c) swap(a,c);
	if(b<c) swap(b,c);
	if(a*n<w){cout << abs(a*n-w) << "\n"; return ;}
	if(c*n>w){cout << abs(c*n-w) << "\n"; return ;}
	ll ans=1e18;
	for(ll x=n;x>=n/2;x--){                       //后一半倒着跑
//		if(x*a+(n-x)*c-w>=ans) break;
		for(ll y=0;y<=n && y<=n-x;y++){
			if(x*a+y*b+(n-x-y)*c-w>=ans) break;
			ll z=n-x-y;
			ans=min(abs(x*a+y*b+z*c-w),ans);
			if(cnt--<0){cout << ans << "\n"; return ;}
		}
	}
	for(ll x=0;x<=n/2;x++){                       //前一半正着跑
		if(x*a+(n-x)*c-w>=ans) break;
		for(ll y=0;y<=n && y<=n-x;y++){
			if(x*a+y*b+(n-x-y)*c-w>=ans) break;
			ll z=n-x-y;
			ans=min(abs(x*a+y*b+z*c-w),ans);
			if(cnt--<0){cout << ans << "\n"; return ;}
		}
	}
	cout << ans << "\n";
}

补 K                                                            牛客传送门

思路:这题能想到要找到左右第一个小于等于自己的史莱姆,答案即为找到的史莱姆的答案+1    此操作可用单调栈实现,单调栈平时做的很少,但并不是复杂的东西,而且方便,当然用线段树也可以但确实没必要

代码如下:

const int N=2e6+10,M=210;
ll n;
int a[N];
int l[N],r[N];
void solve(){
	cin >> n;
	for(int i=1;i<=n;i++) cin >> a[i];
	stack<int>sk;
	for(int i=1;i<=n;i++){
		while(!sk.empty() && a[sk.top()]>a[i]) sk.pop();
		if(!sk.empty()) l[i]=l[sk.top()]+1;
		sk.push(i);
	}
	while(!sk.empty()) sk.pop();
	for(int i=n;i;i--){
		while(!sk.empty() && a[sk.top()]>a[i]) sk.pop();
		if(!sk.empty()) r[i]=r[sk.top()]+1;
		sk.push(i);
	}
	for(int i=1;i<=n;i++) cout << l[i]+r[i] << " ";
}

补 H                                                                  牛客传送门

思路:简单dp,一开始读题没注意到数据范围是 n*m<=3e3,可开三维的状态进行三维的枚举

dp【i】【j】【h】表示走到 ( i , j )时剩余血量 h所用最少魔法次数

代码如下:

ll n;
int a[3030][3030];
vector<vector<vector<int>>>dp;
void solve(){
	int m,h; cin >> n >> m >> h;
	dp=vector<vector<vector<int>>>(n+1,vector<vector<int>>(m+1,vector<int>(h+1010,1e9)));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++)
			cin >> a[i][j];
	}
	dp[1][1][h]=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(i==1 && j==1) continue;
			for(int k=1;k<=h;k++){
				int num1=min(dp[i-1][j][k],dp[i][j-1][k])+1;        //使用魔法
				int num2=min(dp[i-1][j][k+a[i][j]],dp[i][j-1][k+a[i][j]]);  //不使用魔法
				dp[i][j][k]=min(num1,num2);
			}
		}
	}
	cout << *min_element(dp[n][m].begin(),dp[n][m].end());
}

重做道动态开点线段树 19届东南校赛 B           cf传送门

思路:动态开点+线段树上二分,比第一次写的复杂了点

注意MAXN应取2e18,可能会出现前1e18全为1并询问第1e18个0的情况

注意线段树上二分的return,并不是直接return l,而是 l+k-1

更新&纠错:MAXN其实取1e18就够了,因为如下返回

    if(!p || !t[p].sum1) return l+k-1;

在1e18后的点是不会修改到的,所以是全0,可以直接确定要找的位置在 l+k-1

而要直接 return l其实也是可以的,把MAXN开到2e18按理说就没问题,但我这开不了这么多空间如下代码能过应该是因为我的及时return导致实际并没开到1e18之后的点

代码如下:

const int N=3e5+10,M=210;
ll n;
const ll MAXN=2e18+10;
struct seg_Tree{
#define lc(x) t[x].ch[0]
#define rc(x) t[x].ch[1]
	struct nod{
		ll ch[2];
		ll sum0,sum1;
		int tag0,tag1;
	}t[N*155];
	ll root,tot;
	ll ql,qr,qop;
	void pushup(ll p){
		t[p].sum0=t[lc(p)].sum0+t[rc(p)].sum0;
		t[p].sum1=t[lc(p)].sum1+t[rc(p)].sum1;
	}
	void pushdn(ll p,ll l,ll r){
		if(!lc(p)) lc(p)=++tot;
		if(!rc(p)) rc(p)=++tot;
		ll mid=l+r>>1;
		if(t[p].tag0){
			t[lc(p)].sum0=mid-l+1;
			t[lc(p)].sum1=0;
			t[lc(p)].tag0=1;
			t[lc(p)].tag1=0;
			t[rc(p)].sum0=r-mid;
			t[rc(p)].sum1=0;
			t[rc(p)].tag0=1;
			t[rc(p)].tag1=0;
			t[p].tag0=0;
		}
		if(t[p].tag1){
			t[lc(p)].sum1=mid-l+1;
			t[lc(p)].sum0=0;
			t[lc(p)].tag1=1;
			t[lc(p)].tag0=0;
			t[rc(p)].sum1=r-mid;
			t[rc(p)].sum0=0;
			t[rc(p)].tag1=1;
			t[rc(p)].tag0=0;
			t[p].tag1=0;
		}
	}
	void update(ll &p,ll l,ll r){
		if(!p) p=++tot;
		if(ql<=l && qr>=r){
			if(qop==0){
				t[p].sum0=r-l+1;
				t[p].sum1=0;
				t[p].tag0=1;
				t[p].tag1=0;
			}else{
				t[p].sum1=r-l+1;
				t[p].sum0=0;
				t[p].tag1=1;
				t[p].tag0=0;
			}
			return ;
		}
		ll mid=l+r>>1;
		pushdn(p,l,r);
		if(ql<=mid) update(lc(p),l,mid);
		if(qr>mid) update(rc(p),mid+1,r);
		pushup(p);
	}
	void updt(ll l,ll r,ll op){
		ql=l,qr=r;
		qop=op;
		update(root,1,MAXN);
	}
	ll fnd(ll p,ll l,ll r,ll k){
//		if(l==r) return l;      //正确但要开2e18,比较费空间
//		if(l==r) return l+k-1;        //正确
		if(!p || !t[p].sum1) return l+k-1;  //正确
		ll mid=l+r>>1;
		pushdn(p,l,r);
		ll sum=t[lc(p)].sum0;
		if(sum>=k) return fnd(lc(p),l,mid,k);
		else return fnd(rc(p),mid+1,r,k-sum);
	}
}tr;
void solve(){
	int q; cin >> q;
	tr.updt(1,MAXN,0);
	while(q--){
		char op; cin >> op;
		if(op=='?'){
			ll k; cin >> k;
			cout << tr.fnd(tr.root,1,MAXN,k)-1 << "\n";
		}else{
			ll l,r; cin >> l >> r; l++,r++;
			if(op=='+') tr.updt(l,r,1);
			else tr.updt(l,r,0);
		}
	}
}

星期四:

写道线段树练练手 洛谷P1438                       洛谷传送门

思路:区间修改+单点查询,考虑怎么设置懒标记,对于一个点 i,若知道了1操作的 l,k,d,    就能算出加值,为 k + ( i - l )* d,分解一下变为 k + i*d - l*d,那么懒标记可以用 k,d,ld这三个值来实现,不过实际上有点冗余,因为 k和ld都是和sum直接加减的关系,可以合并为一个tag

这题还有一个做法就是线段树维护差分数组,区修+区查

给 l,r区间加上等差数列,可对 l加上k,对 l+1至r加上d,对 r+1减去 k+(len-1)*d,那么这就是一个差分数组,求 p即为 ap+ 差分sum(1-p)

代码如下: 

const int N=2e6+10,M=210;
ll n;
struct seg_Tree{
#define lc p<<1
#define rc p<<1|1
	struct nod{
		int l,r;
		ll sum;
		ll k,d,ld;
	}t[N];
	ll ql,qr,qk,qd;
	nod merge(nod a,nod b){
		nod res;
		res.l=a.l,res.r=b.r;
		res.sum=a.sum+b.sum;
		res.k=res.d=res.ld=0;
		return res;
	}
	void pushup(int p){t[p]=merge(t[lc],t[rc]);}
	void bd(int p,int l,int r){
		t[p]={l,r,0,0,0,0};
		if(l==r){
			cin >> t[p].sum;
			return ;
		}
		int mid=l+r>>1;
		bd(lc,l,mid);
		bd(rc,mid+1,r);
		pushup(p);
	}
	void pushdn(int p){
		if(t[p].k){
			t[lc].sum+=(t[lc].r-t[lc].l+1)*t[p].k;
			t[lc].k+=t[p].k;
			t[rc].sum+=(t[rc].r-t[rc].l+1)*t[p].k;
			t[rc].k+=t[p].k;
			t[p].k=0;
		}
		if(t[p].d){
			t[lc].sum+=(t[lc].l*t[p].d+t[lc].r*t[p].d)*(t[lc].r-t[lc].l+1)/2;
			t[lc].d+=t[p].d;
			t[rc].sum+=(t[rc].l*t[p].d+t[rc].r*t[p].d)*(t[rc].r-t[rc].l+1)/2;
			t[rc].d+=t[p].d;
			t[p].d=0;
		}
		if(t[p].ld){
			t[lc].sum-=(t[lc].r-t[lc].l+1)*t[p].ld;
			t[lc].ld+=t[p].ld;
			t[rc].sum-=(t[rc].r-t[rc].l+1)*t[p].ld;
			t[rc].ld+=t[p].ld;
			t[p].ld=0;
		}
	}
	void update(int p){
		if(ql<=t[p].l && qr>=t[p].r){
			t[p].sum+=(t[p].r-t[p].l+1)*qk;
			t[p].sum+=(t[p].l*qd+t[p].r*qd)*(t[p].r-t[p].l+1)/2;
			t[p].sum-=(t[p].r-t[p].l+1)*ql*qd;
			t[p].k+=qk,t[p].d+=qd,t[p].ld+=ql*qd;
			return ;
		}
		int mid=t[p].l+t[p].r>>1;
		pushdn(p);
		if(ql<=mid) update(lc);
		if(qr>mid) update(rc);
		pushup(p);
	}
	void updt(int l,int r,int k,int d){
		ql=l,qr=r;
		qk=k,qd=d;
		update(1);
	}
	nod query(int p){
		if(ql<=t[p].l && qr>=t[p].r) return t[p];
		int mid=t[p].l+t[p].r>>1;
		pushdn(p);
		if(ql>mid) return query(rc);
		if(qr<=mid) return query(lc);
		return merge(query(lc),query(rc));
	}
	ll ask(int l,int r){
		ql=l,qr=r;
		return query(1).sum;
	}
}tr;
void solve(){
	int q; cin >> n >> q;
	tr.bd(1,1,n);
	while(q--){
		int op; cin >> op;
		if(op==1){
			int l,r,k,d; cin >> l >> r >> k >> d;
			tr.updt(l,r,k,d);
		}else{
			int p; cin >> p;
			cout << tr.ask(p,p) << "\n";
		}
	}
}

下午牛客多校,输麻了

做道差分相关题                                                    牛客传送门

思路:差分真是让人脑子晕晕,a是答案序列的差分数组,b是a的差分数组,c是b的差分数组

代码如下:

const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
ll a[N],b[N],c[N];
void solve(){
	int m; cin >> n >> m;
	for(int i=1;i<=n;i++) a[i]=b[i]=c[i]=0;
	while(m--){
		int tp,pos; cin >> tp >> pos;
		if(tp==1) a[pos]++;
		else if(tp==2)a[pos]++,b[pos+1]++;
		else a[pos]++,b[pos+1]+=3,c[pos+2]+=2;
	}
	for(int i=1;i<=n;i++){
		(c[i]+=c[i-1])%=mod;
		(b[i]+=b[i-1]+c[i])%=mod;
		(a[i]+=a[i-1]+b[i])%=mod;
		cout << a[i];
		if(i<n) cout << " ";
		else cout << "\n";
	}
}

星期五:

补 22上海理工天梯赛 叠硬币                          牛客传送门

寒假就做过,这次又被拿下了

思路:如果不问具体方案,只求最少硬币堆,那就是很简单的dp转移

难点在于如何存下具体的方案,用vector存就别想了,用ai表示高度为 i时最后一个硬币堆

注意要求字典序最小,将 h从大到小排序,方案能换就换

代码如下:

ll n;
int h[3030],dp[3030];
int a[3030];
void solve(){
	int H; cin >> n >> H;
	for(int i=1;i<=n;i++) cin >> h[i];
	sort(h+1,h+n+1,greater<>());
	memset(dp,0x3f,sizeof dp);
	dp[0]=0;
	for(int i=1;i<=n;i++){
		for(int j=H-h[i];j>=0;j--) if(dp[j]<1e9 && dp[j+h[i]]>=dp[j]+1){
			dp[j+h[i]]=dp[j]+1;
			a[j+h[i]]=i;
		}
	}
	if(dp[H]>1e9){cout << -1; return ;}
	cout << dp[H] << "\n";
	int idx=H;
	while(a[idx]) cout << h[a[idx]] << " ",idx-=h[a[idx]];
}

板刷数据结构  最恶心的一集                              牛客传送门

de了一天(真正意义上 的碧油鸡,实际上有半天时间是白给

思路:看似比之前的等差数列复杂一丢,实际上也就复杂一丢,照样推下公式就行了

单点一次更新增加的值为 i^2 + x^2 - 2*x*i (x== l-1,这里建议将 i方,i的和用tag存起来,更方便

有半天时间白给是因为int值相乘爆1e9了没注意到。。。。。

!!!!!!!!!!!!!!!!!!!警钟长鸣!!!!!!!!!!!!!!!!!!!!

代码如下:

const int N=5e5+10,M=210;
const int mod=1e9+7;
ll n;
struct seg_Tree{
#define lc p<<1
#define rc p<<1|1
	struct nod{
		ll l,r;
		ll sum;
		ll tag1,tag2,tag3;
		ll i2,i;
	}t[N<<2];
	ll ql,qr,qx;
	nod merge(nod a,nod b){
		nod res;
		res.l=a.l,res.r=b.r;
		res.sum=(a.sum+b.sum)%mod;
		res.tag1=res.tag2=res.tag3=0;
		res.i2=(a.i2+b.i2)%mod;
		res.i=(a.i+b.i)%mod;
		return res;
	}
	void pushup(int p){t[p]=merge(t[lc],t[rc]);}
	void bd(int p,int l,int r){
		t[p]={l,r,0,0,0,0,0,0};
		if(l==r){
			t[p].i2=1ll*l*l%mod;    /警钟长鸣
			t[p].i=l;
			return ;
		}
		int mid=l+r>>1;
		bd(lc,l,mid);
		bd(rc,mid+1,r);
		pushup(p);
	}
	void pushdn(int p){
		if(t[p].tag1){
			(t[lc].sum+=t[lc].i2*t[p].tag1%mod)%=mod;
			(t[lc].tag1+=t[p].tag1)%=mod;
			(t[rc].sum+=t[rc].i2*t[p].tag1%mod)%=mod;
			(t[rc].tag1+=t[p].tag1)%=mod;
			t[p].tag1=0;
		}
		if(t[p].tag2){
			(t[lc].sum+=(t[lc].r-t[lc].l+1)*t[p].tag2%mod)%=mod;
			(t[lc].tag2+=t[p].tag2)%=mod;
			(t[rc].sum+=(t[rc].r-t[rc].l+1)*t[p].tag2%mod)%=mod;
			(t[rc].tag2+=t[p].tag2)%=mod;
			t[p].tag2=0;
		}
		if(t[p].tag3){
			(t[lc].sum-=t[lc].i*2*t[p].tag3%mod-mod)%=mod;
			(t[lc].tag3+=t[p].tag3)%=mod;
			(t[rc].sum-=t[rc].i*2*t[p].tag3%mod-mod)%=mod;
			(t[rc].tag3+=t[p].tag3)%=mod;
			t[p].tag3=0;
		}
	}
	void update(int p){
		if(ql<=t[p].l && qr>=t[p].r){
			(t[p].sum+=t[p].i2)%=mod;
			(t[p].sum+=(t[p].r-t[p].l+1)*qx%mod*qx%mod)%=mod;
			(t[p].sum-=t[p].i*2*qx%mod-mod)%=mod;
			t[p].tag1++,(t[p].tag2+=qx*qx%mod)%=mod,(t[p].tag3+=qx)%=mod;
			return ;
		}
		int mid=t[p].l+t[p].r>>1;
		pushdn(p);
		if(ql<=mid) update(lc);
		if(qr>mid) update(rc);
		pushup(p);
	}
	void updt(int l,int r){
		ql=l,qr=r,qx=l-1;
		update(1);
	}
	nod query(int p){
		if(ql<=t[p].l && qr>=t[p].r) return t[p];
		int mid=t[p].l+t[p].r>>1;
		pushdn(p);
		if(ql>mid) return query(rc);
		if(qr<=mid) return query(lc);
		return merge(query(lc),query(rc));
	}
	ll ask(int l,int r){
		ql=l,qr=r;
		return (query(1).sum%mod+mod)%mod;
	}
}tr;
void solve(){
	int q; cin >> n >> q;
	tr.bd(1,1,n);
	while(q--){
		int op; cin >> op;
		int l,r; cin >> l >> r;
		if(op==1) tr.updt(l,r);
		else cout << tr.ask(l,r) << "\n";
	}
}

星期六:

练练明天的睿抗,做到了俩题还算有意思

概览 - 2023 睿抗机器人开发者大赛CAIP-编程技能赛-本科组(国赛) (pintia.cn)

搭积木

思路:暴力模拟拆积木能拿22分,正解是把积木抽象为拓扑图,每块积木看作一个点,若两块积木上下直接接触则从上面积木给下面的连条边,只有入度为0的积木能拆出来

代码就不写了,学下思路就行(懒狗跑路

栈与数组

思路:暴力dfs能拿18分,正解为dp

PII dp【i】【j】表示两个栈分别取出前 i个和前 j个的最大数组长度 ma和目前数组长度 now

dp【i】【j】从dp【i-1】【j】转移,取出 ai,先观察dp【i-1】【j】的ma和now是否相等,如果相等,那么塞入 ai必然要使 ma加一,否则 ma不变,再判断是否凑齐 k个,如果满足则 now -= k,  dp【i】【j-1】的转移同理

代码如下:

ll n;
int a[1010],b[1010],sum[2020];
PII dp[1010][1010];
void solve(){
	for(int i=0;i<=2000;i++) sum[i]=0;
	int c1,c2,k; cin >> c1 >> c2 >> k;
	for(int i=1;i<=c1;i++) cin >> a[i];
	for(int i=1;i<=c2;i++) cin >> b[i];
	memset(dp,0x3f,sizeof dp);
	dp[0][1]=dp[1][0]={1,1};
	for(int i=1;i<=c1;i++){
		sum[a[i]]++;
		unordered_map<int,int>mp;
		for(int j=1;j<=c2;j++){
			mp[b[j]]++;
			PII t1={INF,INF},t2={INF,INF};
                //从dp[i-1][j]转移
			auto [x,y]=dp[i-1][j];
			if(x==y) t1.first=x+1;
			else t1.first=x;
			t1.second=y+1;
			if((mp[a[i]]+sum[a[i]])%k==0) t1.second-=k;
			    //从dp[i][j-1]转移
			x=dp[i][j-1].first,y=dp[i][j-1].second;
			if(x==y) t2.first=x+1;
			else t2.first=x;
			t2.second=y+1;
			if((mp[b[j]]+sum[b[j]])%k==0) t2.second-=k;
			dp[i][j]=min(t1,t2);
		}
	}
	cout << dp[c1][c2].first << "\n";
}

补 ABC365 E 典中典之异或和之和                     atc传送门

寒假就写过这题,不过今碰着给忘了,再补一次

思路:容易想到一个O(n^2)的做法,即作异或前缀和, al异或到 ar的值即为 sr异或 s l-1,然后 n^2枚举累加求值,但复杂度是不够的

在前面思路的基础上,我们观察到区间异或的值实际上等价于两个点异或的值,先按位考虑贡献,若   s i-1在第 i位为0, sj在第 i位为1,那么 i - j 这段区间就对答案有 1<<i 的贡献,那么直接按位统计,此位上为0和为1的数的个数cnt0和cnt1,俩俩即为一段贡献为 1<<i 的区间,那么总贡献即 (1<<i)*cnt0*cnt1,注意到题目要求区间长度大于1,于是答案需减去每个数单独的贡献

代码如下:

const int N=5e5+10,M=210;
ll n;
int a[N];
void solve(){
	cin >> n;
	ll sum=0;
	for(int i=1;i<=n;i++){
		cin >> a[i];
		sum+=a[i];
		a[i]^=a[i-1];
	}
	ll ans=0;
	for(int i=0;i<=30;i++){
		int cnt0=0,cnt1=0;
		for(int j=0;j<=n;j++){
			cnt0+=!(a[j]&1<<i);
//			cnt1+=(a[j]&1<<i);   //值可能为 1 2 4 8 16...
			cnt1+=(a[j]>>i&1);
		}
//		cout << cnt0 << " " << cnt1 << "\n";
		ans+=(1ll<<i)*cnt0*cnt1;
	}
	cout << ans-sum;
}

周日:

睿抗被拿下了,世事本阴差阳错,难以预料,不必放在心上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值