星期一:
洛谷 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;
}
周日:
睿抗被拿下了,世事本阴差阳错,难以预料,不必放在心上