CSP-S & NOIP 2025 备考

-std=c++14 -O2 -Wall -Wl,--stack=1000000000

#include<bits/stdc++.h> using namespace std; int main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); int t=0; while(1){ cout<<"test:"<<t++<<"\n"; system("data.exe > data.in"); system("std.exe < data.in > std.out"); system("solve.exe < data.in > solve.out"); if (system("fc std.out solve.out > diff.log")) { cout<<"WA\n"; break; } cout << "AC\n"; } }

1

\(n\) 序列 \(a\)\(a_i\) 可以把自己的值往左右的 \(a\) 转移。最少多少次转移使得所有数相同。\((x,y,k)\) 代表 \(a_x\) 给出 \(d\) 的值给 \(a_y\)。保证有解,并且要求构造字典序最小的一组解。\(n\le3e5\)

保证次数少,一次性把多出来的全部扔一个上面,不操作多次。小根堆维护可分出去数值的位置。把位置靠前的往前放维护字典序。

从左到右扫描序列,遇到受体时,优先用左边的供体补给。若左边没有可用供体,再记录右边的供体。

每个供体最多被加入堆 \(1\) 次,取出 \(1\) 次,堆的插入/取出操作是 \(O (\log n)\),所以复杂度 \(O (n \log n)\)

2

QQ20251028-184031

最小值最大二分。

  • 性质1:区间 \([l,r]\) 的子区间求出的极差绝对没有该区间大。

  • 性质2:所有长度为 \(l\) 的区间,都可以被长为 \(l+1\) 的区间包含。

因此结论:区间长度越长,答案或更优,或不变。

贪心枚举所有长度尽可能长的区间,求出这些区间的极差最值就可以有问题一答案 \(x\)。问题二,二分的条件可以为长度为 \(L\) 的所有连续子序列中极差 \(\geq x\) 的个数是否 \(\geq k\)。用 ST 表很容易做到复杂度 \(O(n\log^2 n)\)

3

QQ20251028-190127

这种题一般单独考虑某个体的贡献。烛火好像写反了,应该是 \(A_i-1\) 个比它小的里选 \(i-1\) 个。

大体意思就是前面的是排好序的一定要把这个数顶到合适位置,后面的随便你。

4

QQ20251028-190352

答案如下。

QQ20251028-190817

点击查看代码
#include<bits/stdc++.h>
#define MAXN 100005
#define P int(1e9+7)
using namespace std;
typedef long long ll;
int n,m;
struct node{
    int id,v;
    node(int id=0, int v=0):id(id), v(v){}

    bool operator < (const node& n1) const{
        return v > n1.v;
    }
};
priority_queue<node> q;
int d[MAXN];

struct edge{
    int v,w;
    edge(int v=0, int w=0):v(v), w(w){}
};

vector<edge> adj[MAXN];
bool vis[MAXN];

void dijkstra(){

    int u,v,w;
    while(!q.empty()){
        u = q.top().id; q.pop();
        if(vis[u]) continue;
        vis[u] = 1;
        for(int k=0;k<adj[u].size();k++){
            v = adj[u][k].v;
            w = d[adj[u][k].w];
            if(d[u] + w < d[v]){
                d[v] = d[u] + w;
                q.push(node(v, d[v]));
            }
        }
    }
}
map<int,int> mp[MAXN];
int main(){
    ios::sync_with_stdio(0);
    freopen("kun.in", "r", stdin);
    freopen("kun.out", "w", stdout);

    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>d[i];
        q.push(node(i, d[i]));
    }

    int a,b,c;
    for(int i=1;i<=m;i++){
        cin>>a>>b>>c;
        //assert(mp[a].count(b)==0);
        adj[a].push_back(edge(c,b));
        adj[b].push_back(edge(c,a));
        //mp[a][b] = c;
    }
    dijkstra();
    for(int i=1;i<=n;i++){
        cout<<d[i]<<" ";
    }
    return 0;
}

5

QQ20251028-191247 \(n\le 6e5\)

答案。

QQ20251028-191313

考虑最终树,比如 \(a_i\) 下一个是 \(b_j\),而 \(a_i\) 连了 \(b_{j+1}\),那么考虑 mst 里面 \(a_i\)\(b_{j+1}\) 谁离 \(b_j\) 近,于是可以和另一条非树边形成一个含 \((a_i,b_{j+1})\) 的环,你发现把\((a_i,b_{j+1})\) 换成那条非树边,mst 变小。

6

QQ20251028-191904\(n\le 1e5\)

答案。

很经典的对关键点建边,好像哪场 NOI Online 也有类似 trick。

QQ20251028-192028

点击查看代码
#include<bits/stdc++.h>
#define MAXN 1600005
#define inf (1e18)
using namespace std;
typedef long long ll;
int n,t,sx,sy,top=0;//n=障碍数,t=查询数,sx/sy=起点坐标
set<pair<int,int>> blk;//存储障碍坐标的集合
int b[MAXN][4];//b[i][k]:节点i在方向k是否有障碍(1=有,0=无)
int dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};//方向数组
pair<int,int> a0[MAXN],ax[MAXN],ay[MAXN],qt[MAXN];//a0=待去重节点,ax=按x排序,ay=按y排序,qt=查询节点
map<pair<int,int>,int> id;//坐标→<int,int>,int> id;//坐标→节点ID的映射(快速查ID)
int dir[MAXN][4];//dir[i][k]:节点i在方向k能到达的相邻节点(0=不可达)
ll d[MAXN];//距离数组(d[i]:到节点i的最短距离,i+top=垂直状态)
struct edge{
    int v,w;
    edge(int v=0,int w=0):v(v),w(w){}
};
vector<edge> adj[MAXN];
void addE(int u,int v,int w){adj[u].push_back(edge(v,w));}
//排序函数:按y升序,y相同按x升序(用于ay数组排序)
bool cmpy(pair<int,int>a,pair<int,int>b){
    return a.second!=b.second?a.second<b.second:a.first<b.first;
}
//01BFS函数:求起点到所有节点的最短距离(双端队列实现)
int q[2*MAXN];//双端队列
void bfs(){
    //初始化距离数组:所有节点设为无穷大
    for(int i=1;i<=2*top;i++)d[i]=inf;
    int l=top,r=top-1;//队列初始化(中间开始防越界)
    int u,v,w;//当前节点u,目标节点v,边权w
    //起点初始化:起点有水平(u=id[起点])和垂直(u+top)两种状态,距离均为1
    u=id[{sx,sy}];d[u]=1;q[++r]=u;
    u+=top;d[u]=1;q[++r]=u;
    //01BFS核心循环:队列非空时处理
    while(l<=r){
        u=q[l++];//取队头节点
        //遍历u的所有出边
        for(int k=0;k<adj[u].size();k++){
            v=adj[u][k].v;w=adj[u][k].w;
            //若经u到v的距离更短,更新距离并调整队列
            if(d[u]+w<d[v]){
                w?q[++r]=v:q[--l]=v;//权0入队头,权1入队尾
                d[v]=d[u]+w;
            }
        }
    }
}
int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    //读入基础数据:起点+障碍
    cin>>n>>t>>sx>>sy;a0[++top]={sx,sy};//起点加入待去重列表
    int x,y,x1,y1;//临时坐标变量
    for(int i=1;i<=n;i++)cin>>x>>y,blk.insert({x,y});//障碍加入集合
    //收集有效节点:障碍周围空位+查询节点(保证能到达)
    for(auto it=blk.begin();it!=blk.end();it++){//遍历障碍,加周围空位
        x=it->first;y=it->second;
        for(int k=0;k<4;k++){
            x1=x+dx[k];y1=y+dy[k];
            if(!blk.count({x1,y1}))a0[++top]={x1,y1};
        }
    }
    for(int i=1;i<=t;i++){//加所有查询节点(避免查询节点不在列表中)
        cin>>x>>y;qt[i]={x,y};a0[++top]={x,y};
    }
    //节点去重:排序后去重,建立坐标→ID映射,初始化障碍方向数组b
    sort(a0+1,a0+1+top);
    top=unique(a0+1,a0+1+top)-a0-1;//更新有效节点数
    memcpy(ax,a0,sizeof(a0));//ax存按x排序的节点
    for(int i=1;i<=top;i++){
        id[a0[i]]=i;//记录节点ID
        x=a0[i].first;y=a0[i].second;
        //检查节点i的4个方向是否有障碍,更新b[i][k]
        for(int k=0;k<4;k++){
            x1=x+dx[k];y1=y+dy[k];
            if(blk.count({x1,y1}))b[i][k]=1;
        }
    }
    sort(a0+1,a0+1+top,cmpy);memcpy(ay,a0,sizeof(a0));//ay存按y排序的节点
    //构建水平方向相邻关系(x相同,左右相邻)
    int u,v,last=1;//last=当前行起始节点索引
    for(int i=1;i<=top;i++){
        if(i<top&&ax[i].first==ax[i+1].first)continue;//未到当前行尾,跳过
        //处理当前行节点,建立左右互通关系
        for(int k=last+1;k<=i;k++){
            u=k;v=k-1;//u=右侧节点,v=左侧节点
            if(!b[u][3]&&!b[v][1]){//u左无障且v右无障,可互通
                dir[u][3]=v;dir[v][1]=u;
            }
        }
        last=i+1;//更新下一行起始索引
    }
    //构建垂直方向相邻关系(y相同,上下相邻)
    last=1;//last=当前列起始节点索引
    for(int i=1;i<=top;i++){
        if(i<top&&ay[i].second==ay[i+1].second)continue;//未到当前列尾,跳过
        //处理当前列节点,建立上下互通关系
        for(int k=last+1;k<=i;k++){
            u=id[ay[k]];v=id[ay[k-1]];//u=下方节点ID,v=上方节点ID
            if(!b[u][2]&&!b[v][0]){//u上无障且v下无障,可互通
                dir[u][2]=v;dir[v][0]=u;
            }
        }
        last=i+1;//更新下一列起始索引
    }
    //构建图的边(相邻节点边+状态转换边)
    for(int i=1;i<=top;i++){
        x=ax[i].first;y=ax[i].second;
        for(int k=0;k<4;k++){
            //1.相邻节点边:方向k有可达节点,加权0边(同状态内移动)
            if(dir[i][k]){
                if(k%2)addE(i+top,dir[i][k]+top,0);//k=1/3(水平方向),对应垂直状态
                else addE(i,dir[i][k],0);//k=0/2(垂直方向),对应水平状态
            }
            //2.状态转换边:方向k有障碍,加权1边(切换水平/垂直状态)
            x1=x+dx[k];y1=y+dy[k];
            if(blk.count({x1,y1})){
                if(k%2)addE(i+top,i,1);//k=1/3(水平障碍),垂直→水平
                else addE(i,i+top,1);//k=0/2(垂直障碍),水平→垂直
            }
        }
    }
    //执行01BFS求最短距离
    bfs();
    //处理查询并输出结果
    ll ans;
    for(int i=1;i<=t;i++){
        //查询节点是起点,直接输出0
        if(qt[i].first==sx&&qt[i].second==sy){cout<<"0\n";continue;}
        u=id[qt[i]];//获取查询节点ID
        ans=min(d[u],d[u+top]);//取水平/垂直状态的最短距离
        cout<<(ans==inf?-1LL:ans)<<"\n";//无穷大输出-1,否则输出距离
    }
    return 0;
}

7

一张图,每次可以删一条边重建。最少几次变为连通图,给出字典序最小的实现 \((i,j,u,v)\)。即边 \((i,j)\rightarrow(u,v)\)\(n\le 1e5,m\le 2e5\)

QQ20251028-202159

8

给定一个 \(n\) 个点 \(m\) 条边的有边权无向图,每个点有颜色在 \([1,K]\) 之间。\(q\) 次操作,每次更改一个点的颜色,然后询问不同颜色之间的点的最短距离。保证每次至少有两种颜色。

\(1 \le n,q \le 2\times10^5,1 \le m \le 4\times10^5,1 \le K \le 10^6\)

Solution

不是很好想的一道题。

首先题目询问的其实就是最短的一条边,满足两端颜色不同。

但是每次修改的边很多,不方便维护。

不难发现答案中出现的边一定是在最小生成树上的边。因为如果有一个答案不在最小生成树上,而它两端颜色不同,那么最小生成树上一定存在一条边,两端颜色不同且权值比这条边小。

那么现在只需要维护一个树。树的好处就是:改变一个点的权值,只会影响父亲和儿子。儿子有很多,但是我们可以在父亲节点上维护儿子的信息。

(下面实现的数据结构可能有多种)

这题主要是和颜色有关,那么我们记录颜色。改变一个点 \(x\) 之后,要找到儿子中颜色和 \(x\) 不同且边权最小的一条边。于是要记录它儿子中某个颜色的所有边,同一个颜色的儿子的边权存在一个 multiset 中。然后还要查询除了 \(x\) 的颜色以外最小的边权。那么还是需要一个数据结构,这可以用一个动态开点的线段树实现(下标为颜色,查询的时候也就是询问 \([1,y-1]\cup [y+1,K]\) 的最小值。)

注意 multiset 只在叶子节点用到,不需要开很多。

然后改变一个点 \(x\),他还会影响他的父亲。对于这个我们可以在父亲的线段树中先删去原来的颜色,再加入新的颜色,然后重新询问一遍。

那么此时知道了每个点到儿子的最小合法的边。接着用一个全局的 multiset 维护,维护每个点到它儿子的边中最小颜色不同的边。每次修改都要在这个 multiset 里修改。只需要先把原来的答案删掉,改完之后再询问一遍放进去就可以了。

\(O((n+q)\log K)\)

一个小优化:可以把最小生成树上的边离散,然后全局可以用 BIT 维护(询问的时候在BIT上二分),每个点的 multiset 就可以换成 set了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=200010,M=400010;
struct Edge{
	int x,y,z;
	bool operator <(const Edge &_)const{
		return z<_.z;
	}
}E[M];
struct edge{
	int nxt,t,s;
}e[N<<1];
int head[N],edge_cnt;
void add_edge(int x,int y,int z){
	e[edge_cnt]=(edge){head[x],y,z};
	head[x]=edge_cnt++;
}
int B[N],h;
struct Binary_Indexed_Tree{
	int bit[N];
	void Add(int i,int x){
		while (i<=h){
			bit[i]+=x;
			i+=i&-i;
		}
	}
	int Query(){
		int i,res=0;
		for (i=17;i>=0;i--){
			int t=res|(1<<i);
			if (t<=h && bit[t]==0){
				res=t;
			}
		}
		return res+1;
	}
}BIT;
set<int>G[N<<1];
struct Segment_Tree{
	static const int O=N*40;
	#define Lc lson[p]
	#define Rc rson[p]
	int lson[O],rson[O],T_Cnt,Mn[O],ID[O],S_Cnt;
	void Insert(int l,int r,int &p,int x,int y){
		if (p==0){
			p=++T_Cnt;
			Mn[p]=h+1;
			if (l==r){
				ID[p]=++S_Cnt;
				G[ID[p]].insert(h+1);
			}
		}
		if (l==r){
			G[ID[p]].insert(y);
			Mn[p]=*G[ID[p]].begin();
			return;
		}
		int mid=(l+r)>>1;
		if (x<=mid){
			Insert(l,mid,Lc,x,y);
		}else{
			Insert(mid+1,r,Rc,x,y);
		}
		Mn[p]=min(Mn[Lc],Mn[Rc]);
	}
	void Delete(int l,int r,int p,int x,int y){
		if (l==r){
			G[ID[p]].erase(y);
			Mn[p]=*G[ID[p]].begin();
			return;
		}
		int mid=(l+r)>>1;
		if (x<=mid){
			Delete(l,mid,Lc,x,y);
		}else{
			Delete(mid+1,r,Rc,x,y);
		}
		Mn[p]=min(Mn[Lc],Mn[Rc]);
	}
	int Query(int l,int r,int p,int pl,int pr){
		if (p==0){
			return h+1;
		}
		if (l==pl && r==pr){
			return Mn[p];
		}
		int mid=(l+r)>>1;
		if (pr<=mid){
			return Query(l,mid,Lc,pl,pr);
		}else if (pl>mid){
			return Query(mid+1,r,Rc,pl,pr);
		}else{
			return min(Query(l,mid,Lc,pl,mid),Query(mid+1,r,Rc,mid+1,pr));
		}
	}
	#undef Lc
	#undef Rc
}T;
int K,Rt[N];
int Query(int x,int y){
	int res=h+1;
	if (y>1){
		res=min(res,T.Query(1,K,Rt[x],1,y-1));
	}
	if (y<K){
		res=min(res,T.Query(1,K,Rt[x],y+1,K));
	}
	return res;
}
int A[N],fa[N],Val[N];
bool Leaf[N];
void dfs_pre(int x,int f){
	fa[x]=f;
	int i;
	Leaf[x]=1;
	for (i=head[x];~i;i=e[i].nxt){
		int to=e[i].t,val=e[i].s;
		if (to==f){
			continue;
		}
		Val[to]=val;
		dfs_pre(to,x);
		T.Insert(1,K,Rt[x],A[to],val);
		Leaf[x]=0;
	}
	if (!Leaf[x]){
		BIT.Add(Query(x,A[x]),1);
	}
}
int Fa[N];
int getfa(int x){
	if (Fa[x]==x){
		return x;
	}else{
		return Fa[x]=getfa(Fa[x]);
	}
}
int main(){
	int n,m,q,i;
	scanf("%d%d%d%d",&n,&m,&K,&q);
	memset(head+1,-1,sizeof(int)*1*n);
	for (i=1;i<=m;i++){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		E[i]=(Edge){x,y,z};
	}
	for (i=1;i<=n;i++){
		scanf("%d",&A[i]);
	}
	sort(E+1,E+1+m);
	for (i=1;i<=n;i++){
		Fa[i]=i;
	}
	for (i=1;i<=m;i++){
		int x=E[i].x,y=E[i].y,z=E[i].z;
		int p=getfa(x),q=getfa(y);
		if (p!=q){
			Fa[p]=q;
			B[++h]=z;
			add_edge(x,y,h);
			add_edge(y,x,h);
		}
	}
	T.Mn[0]=h+1;
	dfs_pre(1,0);
	for (i=1;i<=q;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		if (A[x]==y){
			printf("%d\n",B[BIT.Query()]);
			continue;
		}
		if (!Leaf[x]){
			BIT.Add(Query(x,A[x]),-1);
			BIT.Add(Query(x,y),1);
		}
		if (fa[x]!=0){
			BIT.Add(Query(fa[x],A[fa[x]]),-1);
			T.Delete(1,K,Rt[fa[x]],A[x],Val[x]);
			T.Insert(1,K,Rt[fa[x]],y,Val[x]);
			BIT.Add(Query(fa[x],A[fa[x]]),1);
		}
		A[x]=y;
		printf("%d\n",B[BIT.Query()]);
	}
	return 0;
}

9

QQ20251028-203018

\(n,q,w,a \le 1e5\)

QQ20251028-220421

10

QQ20251028-214433

答案。

QQ20251028-214446

QQ20251028-214457

11

QQ20251028-220645

具体就是离散化,\(t_{a_i}\) 即以 \(a_i\) 结尾的最长递增子序列。对于一个 \(a_i\),找左侧最大的 \(t_{a_j}\) 满足 \(a_j<a_i\) 更新。树状数组区间 \(\max\) 和区间加即可。

12

QQ20251028-220952

诈骗题。这个 \(\left |a_i-b_j \right |\) 范围只有 \(3e6\)。枚举 \(\sqrt{\left |a_i-b_j \right |}\) 然后 \(\times\) 该差值开根后的出现次数即可。

\(t\) 统计一下偏移后的 \(a,b\) 各个数字出现次数。这里的偏移是指统一减去 \(min\) 值。然后 \(d=\sqrt{\left |a_i-b_j \right |}\) 出现次数就是 \(\sum a_{x+d}\times b_x+\sum b_{x+d}\times a_x\)\(d\) 的范围不到 \(2000\),能轻松通过。

13

P4653 有两组物品,每个物品都有一定的价值,你需要选择若干个物品,收益为两组物品中价值和较少的那组物品的价值和减去所选取的所有物品数。使收益最大化。

贪心。最大化价值考虑先选大的,尽量维持两堆 \(S\) 平均保持最小值不要相差太大。双指针维护。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define For(i,l,r) for(int i=l;i<=r;i++)
int n;
const int N=1e5+10;
#define db double
db a[N],b[N];
db ans;
db sa,sb;
int main(){
	cin>>n;
	For(i,1,n)cin>>a[i]>>b[i];
	sort(a+1,a+n+1);sort(b+1,b+n+1);
	reverse(a+1,a+n+1);reverse(b+1,b+n+1);
//	For(i,1,n)cout<<a[i]<<" ";
//	cout<<"\n";
	int l=0;
	For(r,1,n){
		sb+=b[r];
		int len=l+r;
		ans=max(ans,min(sa-len,sb-len));//价值和-长度
		while(sa<sb&&l<n){
			l++;
			sa+=a[l];
			len=l+r;
			ans=max(ans,min(sa-len,sb-len));//价值和-长度
		}
	}
	cout<<fixed<<setprecision(4)<<ans;
	return 0;
}

14

P1578 一个矩形内有一些点,找最大面积的子矩阵满足内部不含任意一个点(边缘可以包含)。

分类讨论的扫。

  • 左边以障碍点为边界。
  • 右边以障碍点为边界。
  • 左右以牛场为边界。

扫的过程就是从左往右,维护纵坐标的 \(\operatorname{max,min}\) 然后计算最大子矩阵。要点在于需要判断何时这个纵坐标贡献给 \(\operatorname{max/min}\)。具体详情见代码。

点击查看代码
#include<bits/stdc++.h> 
using namespace std; 
#define int long long 
#define For(i,l,r) for(int i=l;i<=r;i++) 
int l,w,n;
const int N=5010;
struct node{
    int x,y;
}p[N];
bool cmp(node a,node b){
    return a.x<b.x;
}
int ans;
bool cmp2(node a,node b){
    return a.x>b.x;
}
bool cmp3(node a,node b){
    return a.y<b.y;
}
signed main(){ 
    ios::sync_with_stdio(0); 
    cin.tie(0);cout.tie(0); 
    cin>>l>>w>>n;
    if(n==0){
        cout<<l*w;
        return 0;
    }
    For(i,1,n)cin>>p[i].x>>p[i].y;
    p[++n].x=0,p[n].y=0;
    p[++n].x=0,p[n].y=w;
    p[++n].x=l,p[n].y=0;
    p[++n].x=l,p[n].y=w;
    sort(p+1,p+n+1,cmp);
    int mx,mn;
    For(i,1,n){
        mx=0;mn=w;
        For(j,i+1,n){
            ans=max(ans,abs((p[i].x-p[j].x)*(mx-mn)));
            if(p[j].y<p[i].y)mx=max(mx,p[j].y);
            else mn=min(mn,p[j].y);
        }
    }
    sort(p+1,p+n+1,cmp2);
    For(i,1,n){
        mx=0;mn=w;
        For(j,i+1,n){
            ans=max(ans,abs((p[i].x-p[j].x)*(mx-mn)));
            if(p[j].y<p[i].y)mx=max(mx,p[j].y);
            else mn=min(mn,p[j].y);
        }
    }
    sort(p+1,p+n+1,cmp3);
    For(i,1,n-1){
        ans=max(ans,(p[i+1].y-p[i].y)*l);
    }
    cout<<ans;
    return 0; 
}

15

P3467 给定若干长方形的宽和高,排成一排,最少多少个矩形可以完全覆盖。这篇文章说的很清楚。下面附上代码。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,ans;
const int N=250010;
ll d,w;
stack<ll>s;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;ans=n;
	for(int i=1;i<=n;i++){
		cin>>d>>w;
		while(!s.empty()&&s.top()>=w){
			if(s.top()==w)ans--;
			s.pop();
		}
		s.push(w);
	}
	cout<<ans;
	return 0;
}

16

T22526 给你一个长为 \(n\) 的序列 \(a\) 和一个常数 \(k\)。有 \(m\) 次询问,每次查询一个区间 \([l,r]\) 内所有数最少分成多少个连续段,使得每段的和都 \(\le k\)

贪心,每一段尽量选长的。不合法就是存在区间内某个数 \(> k\),否则可以分成一个一个的。然后可以双指针预处理每个数字一段最远到哪里。倍增一下处理第 \(2^j\) 段到哪里。最后对于 \([l,r]\) 只有 \(O(\log n)\) 的查询复杂度。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=1e6+10;
int n,m;ll k;
ll a[N],sum[N];
int r[N],f[20][N];
int mx[N][20];
int qry(int l,int r){
    int len=r-l+1,t=0;
    while((1<<(t+1))<=len)t++;
    return max(mx[l][t],mx[r-(1<<t)+1][t]);
}
void solve(int l,int r){
    if(qry(l,r)>k){cout<<"Chtholly\n";return;}
    ll s=sum[r]-sum[l-1];
    if(s<=k){cout<<"1\n";return;}
    int pos=l,ans=0;
    for(int j=19;j>=0;j--){
        if(f[j][pos]<=r){
            ans+=(1<<j);
            pos=f[j][pos];
        }
    }
    if(pos<=r)ans++;
    cout<<ans<<"\n";
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n>>m>>k;
    For(i,1,n){
        cin>>a[i];
        sum[i]=sum[i-1]+a[i];
    }
    For(i,1,n)mx[i][0]=a[i];
    For(j,1,19){
        for(int i=1;i+(1<<j)-1<=n;++i){
            mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]);
        }
    }
    int j=n+1;
    ll s=0;
    for(int i=n;i>=1;i--){
        s+=a[i];
        while(s>k&&j>i){j--;s-=a[j];}
        r[i]=j-1;
        if(r[i]>n)r[i]=n;
    }
    For(i,1,n){
        f[0][i]=r[i]+1;
        if(f[0][i]>n)f[0][i]=n+1;
    }
    For(j,1,19){
        For(i,1,n){
            if(f[j-1][i]>n)f[j][i]=n+1;
            else f[j][i]=f[j-1][f[j-1][i]];
        }
    }
    while(m--){
        int l,r;cin>>l>>r;
        solve(l,r);
    }
    return 0;
}

17

P3017 \(R \times C\) 的网格。格子有权值。可以水平切 \(A-1\) 刀分出 \(A\) 块。每个分出的水平块里再独立竖直切 \(B-1\) 刀分成 \(B\) 块。要使每块的最小权值最大,求这个值。

最小值最大考虑二分后判可行性。具体解法可以看这篇

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=510;
int R,C,A,B;
int s[N][N];
bool chk(int x){
	int lst1=0,cnt=0;
	For(i,1,R){
		int sum=0,lst2=0;
		For(j,1,C){
			if(s[i][j]-s[lst1][j]-s[i][lst2]+s[lst1][lst2]>=x){
				sum++;lst2=j;
			}
		}
		if(sum>=B){cnt++;lst1=i;}
	}
	return (cnt>=A);
}
int main(){
	cin>>R>>C>>A>>B;
	For(i,1,R){
		For(j,1,C){
			int x;cin>>x;
			s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+x;
		}
	}
	int l=0,r=1e9,ans;
	while(l<=r){
		int mid=(l+r)>>1;
		if(chk(mid)){
			l=mid+1;
			ans=mid;
		}
		else r=mid-1;
	}
	cout<<ans;
	return 0;
}

18

P4552 \({a_1,a_2,\cdots,a_n}\),每次可以选择一个区间\([l,r]\),使这个区间内的数都加 \(1\) 或者都减 \(1\)。 问至少几次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列有多少种。

结论题。答案分别是 \(\max(p,q)\)\(\left |p-q \right |+1\)

点击查看代码
#include<stdio.h>
#include<algorithm>
using namespace std;
const int N=1e5+10;
long long n,p,q,a[N],b[N];
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		b[i]=a[i]-a[i-1];
	}
	for(int i=2;i<=n;i++){
		if(b[i]<0)p-=b[i];
		if(b[i]>0)q+=b[i];
	}
	printf("%lld\n%lld\n",max(p,q),abs(p-q)+1);
	return 0;
}

19

P1966 给定两数列 \(a,b\),两序列中数字可以两两交换。问保证 \(\sum(a_i-b_i)^2\) 最小时的最小交换次数是多少。

拆式子得到一部分定值和另一部分 \(-2\times a \times b\)。经典结论是当 \(a\) 中第 \(k\) 小匹配 \(b\) 中第 \(k\) 小时得到最值。然后我们可以把它们离散化,\(a\) 钦定为 \(1 \rightarrow n\) 然后让 \(b\) 按照对应数字映射过来。最终答案是 \(b\) 中逆序对数量。

20

P13778 比赛共 \(n\) 题。初始信心值为 \(k\),只有信心值恰好为 \(c_i\) 时才会做第 \(i\) 道题。按任意排列题目,按顺序开题。会做当前题则 \(k\) 增加 \(1\),否则 \(k\) 减小 \(1\)。问最多能做出来几题。

题解链接。补充:其被选择的时间点恰好为 \(k−c_i+2t+1\) 是根据计算得来,设当前操作到第 \(p\) 步,做对 \(t\),做错了 \(p-1-t\),则当前权值为 \(k-(p-t)+t+1=k-p+2t+1=c_i\),所以 \(p=k+2t-c_i+1\)。(这个 \(+1\) 是因为 \(c_i\) 的上一题)。

\(a_j \geq a_i-1\) 带入定义式算一下就出来了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define For(i,l,j) for(int i=l;i<=j;i++)
const int N=1e6+10;
int n,k,cnt,c[N],a[N];
void solve(){
	cin>>n>>k;cnt=0;//不可选数目
	For(i,1,n)cin>>c[i],a[i]=k+1-c[i];//a就是p
	sort(a+1,a+n+1);
	For(i,1,n){
		int j=i;
		while(j<n&&a[j+1]<=a[j]+1)j++;//若下个元素与当前元素的差<=1,则拓展
		if(a[j]<=0){i=cnt=j;continue;}//最大位置索引<=0,不合法
		if(2*(j-cnt-1)+a[i]>n){
        //检查当前区间最多可选多少题
        //若选择(j-cnt)题(当前区间排除不可选后的最大可能数),最后一个位置是否超过n
        //最后一个位置为:a[i](区间最小a)+2*(j-cnt-1)(t的最大值)
            cout<<max((n-a[i])/2+1,i-cnt-1)<<"\n";
            //(n-a[i])/2+1:由位置<=n推导的最大数量
            //i-cnt-1:当前区间前已选的数量
            return;
        }
		i=j;//当前区间的题均可选,i跳至j(下一轮从j+1开始)
	}
    cout<<n-cnt<<"\n";//n-不合法个数
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    int T;cin>>T;
    while(T--)solve();
    return 0; 
}

21

P4375

\(a\) 已离散化,答案是:

\[\max_{i=1}^n \{\sum_{j=1}^i [a_j>i]\} \]

这个式子其实就是在 \([1,i]\) 中不属于这个区间的数的个数。

如果 \([1,i]\) 没有排序好,左冒泡走掉的肯定不属于这个区间,并且右冒泡肯定会补回来一个,所以单独对于这个区间来说就需要 \(\sum_{j=1}^i [a_j>i]\) 的次数。注意事项:至少运行 \(1\) 次,本题如果使用离散化,不能把相同的数弄成同一个值,要按照位置弄成从小到大不同的值。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=1e6+10;
ll n,t[N],ans=1;
struct node{ll v,id;}a[N];
bool cmp(node a,node b){return a.v<b.v;}
bool cmp2(node a,node b){return a.id<b.id;} 
void add(ll x,ll v){for(;x<=n;x+=x&(-x))t[x]+=v;}
ll qry(ll x){
	ll res=0;for(;x;x-=x&(-x))res+=t[x];
	return res;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;For(i,1,n){cin>>a[i].v;a[i].id=i;}
	sort(a+1,a+n+1,cmp);For(i,1,n)a[i].v=i;
	sort(a+1,a+n+1,cmp2);
	For(i,1,n){
		add(a[i].v,1);
		//a[j]>i有几个 (1<=j<=i)
		ans=max(ans,i-qry(i));
	}
	cout<<ans;
	return 0;
}

22

一次考试共有 \(n\) 个人参加,可能出现多个人成绩相同的情况。第 \(i\) 个人说有 \(a_i\) 个人成绩比他高,\(b_i\) 个人比他低。请求出最少有几个人说慌。\(1 \leq n \leq 10^5\)\(0 \leq a_i, b_i \leq n\)

考虑当前这个数的排名,从高到低,这人排名区间在 \([a+1,n-b]\) 之间。map 记录每段排名区间有几人,\(f_i\) 为前 \(i\) 个排名中最多说真话的人数。答案为 \(n-f_i\)。对于每个 \(i\) 遍历以它结尾的区间的开头,这段区间对真话人数的贡献就是,区间总人数和区间长度的最小值(长度不够放不了这么多人)。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=1e5+10;
int n;
int f[N];
map<pair<int,int>,int> mp;
vector<int>g[N];
int main(){
	cin>>n;
	For(i,1,n){
		int a,b;
		cin>>a>>b;
		a=a+1;b=n-b;
		if(a>b)continue;
		if(!mp[make_pair(a,b)])g[b].push_back(a);
		mp[make_pair(a,b)]++;
	}
	For(i,1,n){
		f[i]=f[i-1];
		for(int j=0;j<g[i].size();j++){
			int l=g[i][j];
			f[i]=max(f[i],f[l-1]+min(mp[make_pair(l,i)],i-l+1));
		}
	}
	cout<<n-f[n];
	return 0;
}

23

三个正整数 \(n,m,k\)\(n\) 个点,\(m\) 条路,感兴趣的城市的个数 \(k\)

\(m\) 行,每行包括 \(3\) 个正整数 \(x,y,z\),表示从第 \(x\rightarrow y\) 的单向道路长度为 \(z\)\(x,y\) 可能相等,一对 \(x,y\) 也可能重复出现。

接下来一行包括 \(k\) 个正整数,表示感兴趣的城市的编号。求这些感兴趣城市两两之间最短路的最小值。

\(2 \le k \le n\)\(1 \le x,y \le n\)\(1 \le z \le 2 \times 10^9\)\(T \leq 5\)

sol。

特殊点分成 \(A,B\) 两个集合,新建 \(s\rightarrow A\) 的所有点,\(t\rightarrow B\) 的所有点,边权 \(0\)。那么 \(s\)\(t\) 的最短路就是 \(A,B\) 集合点之间的最短路的最小值。

那么对于 \(k\) 个特殊点,我们枚举二进制里的第 \(i\) 位,把二进制第 \(i\) 位是 \(0\) 的点放在 \(A\)\(1\) 的点放在 \(B\) ,用以上方法跑一个最短路。

然后跑 \(log\ n\) 次最短路之后,所有最短路的最小值就是最终答案。

因为是有向图,所以要跑两遍,第一遍,起点连 \(A\),终点连 \(B\)。第二遍反过来。

原理是,假设 \(k\) 个特殊点里最近的是 \(x\)\(y\) ,那么 \(x\)\(y\) 一定有一个二进制位不一样,那么他们肯定在那次分组的时候被放进了不同的集合,从而肯定被算进了最后的答案之中最短路。

sol2。

只要证明最优解可以被枚举到而且不存在不合法的更短解即可。

这题直接染色正反跑两遍 \(dij\) 即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)

int n,m,k;  // n:城市数, m:道路数, k:感兴趣的城市数
const int N=1e5+10;  // 城市最大数量
const int M=5e5+10;  // 道路最大数量

// 邻接表存储边:每个边有下一条边的索引(nxt)、终点(to)、长度(d)
struct node{
    int nxt,to,d;
}e[M];
int h[N],cnt;  // h[u]:u的邻接表头, cnt:边的计数器

// 添加一条从u到v,长度为w的边
void add(int u,int v,int w){
    e[++cnt].nxt=h[u];  // 新边的下一条指向当前u的表头
    e[cnt].to=v;        // 终点
    e[cnt].d=w;         // 长度
    h[u]=cnt;           // u的表头更新为新边
}

int x[M],y[M],z[M];  // 存储所有边的原始信息: x[i]→y[i],长度z[i]
int a[N];  // 存储k个感兴趣的城市编号
ll d[2][N];  // d[0][u]:正向图中u到最近感兴趣城市的距离; d[1][v]:反向图中v到最近感兴趣城市的距离
int col[2][N];  // col[0][u]:正向图中u最近的感兴趣城市; col[1][v]:反向图中v最近的感兴趣城市
bool vis[N];  // Dijkstra中标记是否访问过

// 清空邻接表
void clear(){
    For(i,1,n)h[i]=0;
    cnt=0;
}

// 多源Dijkstra算法:计算每个点到最近的感兴趣城市的距离(dist)和对应城市(color)
void dij(ll *dist,int *color){
    // 优先队列(大根堆取负变成小根堆):存储(距离, 节点),按距离从小到大取
    priority_queue<pair<ll,int> >q;
    // 初始化:所有点距离设为无穷大,未访问
    For(i,1,n){
        vis[i]=0;
        dist[i]=1e18;
    }
    // 所有感兴趣的城市作为起点:距离0,对应城市是自己
    For(i,1,k){
        dist[a[i]]=0;
        color[a[i]]=a[i];
        q.push(make_pair(0,a[i]));  // 入队
    }
    // 执行Dijkstra
    while(!q.empty()){
        int u=q.top().second;  // 当前距离最小的节点
        q.pop();
        if(vis[u])continue;  // 已处理过,跳过
        vis[u]=1;
        // 遍历u的所有出边
        for(int i=h[u];i;i=e[i].nxt){
            int v=e[i].to;    // 终点v
            int w=e[i].d;     // 边长度w
            // 若v通过u到最近感兴趣城市的距离更短,更新
            if(dist[v]>dist[u]+w){
                dist[v]=dist[u]+w;
                color[v]=color[u];  // v的最近感兴趣城市和u相同
                q.push(make_pair(-dist[v],v));  // 入队(负号维持小根堆)
            }
        }
    }
}

void solve(){
    cin>>n>>m>>k;
    clear();  // 清空正向图
    // 读入m条边,建正向图
    For(i,1,m){
        int u,v,w;
        cin>>u>>v>>w;
        if(u!=v)add(u,v,w);  // 忽略自环(对最短路无意义)
        x[i]=u;y[i]=v;z[i]=w;  // 保存边的原始信息
    }
    // 读入k个感兴趣的城市
    For(i,1,k)cin>>a[i];
    // 正向图跑多源Dijkstra:得到d[0]和col[0]
    dij(d[0],col[0]);
    
    clear();  // 清空,准备建反向图
    // 建反向图(所有边方向反转)
    For(i,1,m){
        if(x[i]!=y[i]){  // 忽略自环
            add(y[i],x[i],z[i]);  // 原x→y变成y→x
        }
    }
    // 反向图跑多源Dijkstra:得到d[1]和col[1]
    dij(d[1],col[1]);
    
    // 找最小的两两最短路
    ll ans=1e18;
    // 遍历所有边,计算可能的跨感兴趣城市的路径
    For(i,1,m){
        int u=x[i],v=y[i],w=z[i];  // 原边u→v,长度w
        // 若u的最近感兴趣城市和v的最近感兴趣城市不同(避免同一点)
        if(col[0][u]!=col[1][v]){
            // 路径: col[0][u] → ... → u → v → ... → col[1][v]
            // 总长度: d[0][u] (col到u) + w (u→v) + d[1][v] (v到col)
            ans=min(ans,d[0][u]+d[1][v]+w);
        }
    }
    cout<<ans<<"\n";
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    int T;cin>>T;  // T组测试数据
    while(T--)solve();
    return 0;
}

24

\(n\) 的可重序列 \(a\),找最长一段数字,满足该段数字排序完形如 \(1,2,3\cdots k\)\(n\le 1e6\)

找到 \(1\) 的位置往两侧拓展,注意没有 \(1\) 直接判 \(0\)。给每个数值 \(a_i\) 赋一个异或值。先向左拓展,记录前缀异或。再向右,不断异或并记录最大值。需要满足以下要求。

  • $mx\geq $ 右侧扩展长度 \(j-i+1\):子数组总长度至少为 \(mx\)(因为要包含 \(1\rightarrow mx\))。
  • $mx\le $ 右侧长度 \(+\) 左侧长度 \(j-i+1+len\):子数组总长度足够容纳 \(1\rightarrow mx\)

如果哪段前缀满足了就计入,同理先算右边前缀,再拓展左边算一遍。

看代码更清晰。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=1e6+10;
int n,a[N];
int ans;
int rd[N];
int s[N];
int l[N];
mt19937 rnd(time(0));
void solve(){
    For(i,1,n){
    	if(a[i]!=1)continue;
    	int j;
        for(j=i-1;a[j]!=1;j--){
            l[i-j]=l[i-j-1]^rd[a[j]];
        }
        int mx=1;
        int len=i-j-1;
        int r=rd[1];
        for(j=i+1;a[j]!=1;j++){
            mx=max(mx,a[j]);
            r^=rd[a[j]];
            //当前最大数字mx需在合理范围
            //mx>=右侧扩展长度(j-i+1):子数组总长度至少为mx(因为要包含1~mx)
            //mx<=右侧长度+左侧长度(j-i+1+len):子数组总长度足够容纳1~mx
            if(mx>=j-i+1&&mx<=j-i+1+len){
                if(s[mx]==(r^l[mx-(j-i+1)])){
                    ans=max(ans,mx);
                }
            }
        }
	}
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    a[0]=1;
    int T;cin>>T;
    while(T--){
        cin>>n;
        ans=0;
        a[n+1]=1;
        For(i,1,n){
            cin>>a[i];
            if(a[i]==1)ans=1;
            rd[i]=rnd();
            s[i]=s[i-1]^rd[i];
        }
        if(!ans){
            cout<<"0\n";
            continue;
        }
        solve();reverse(a+1,a+n+1);solve();
        cout<<ans<<"\n";
    }
    return 0;
}

25

QQ20251029-204921

26

两个栈,每个栈中均有 \(n\times K\) 个元素,其中数字 \(1,2,\cdots, n\) 各分别出现了恰好 \(K\) 次。

不断进行如下操作,直到有一个栈变为空:

  • 如果当前两个栈的栈顶元素相同,则将两个栈的栈顶元素都弹出,获得 \(1\) 的得分。
  • 如果当前两个栈的栈顶元素不同,则选择一个栈的栈顶元素弹出,不得分。

计算通过合理的操作,最大得分是多少。

对所有数据,保证 \(1\le n\le 10^4,1\le K\le 16\)


考虑所有得分的时刻,把配对上的两个元素在原栈中的位置标出来,那么得到两个单调递增序列 \(x,y\)。其中满足 \(a_{x_i}=b_{y_i}\)

反之,如果两个序列满足上面的要求,那么就可以通过⼀定的操作得到分数。于是目标就变成了在满足条件下最大化序列⻓度。

把所有可能的 \((x_i,y_i)\) 拿出来,定义全序关系 \(i < j\) 时有 \((x_i,y_i)<(x_j,y_j)\)。(这里的小于是两维度都小于)。共 \(nK^2\) 对。

为什么是 \(nK^2\) 对?因为共 \(n\) 个数字,每个数字在第一个栈里出现 \(K\) 次,也在第二个里出现 \(K\) 次。

找最长上升子序列即可。别被诈骗了。

#include<bits/stdc++.h>
using namespace std;
#define For(i,l,r) for(int i=l;i<=r;i++)
const int N=2e5+10;
int n,k,a[N],ans;
int t[N],p[N][20],cnt[N];
inline int qry(int x){
	int res=0;
	for(;x;x-=x&(-x))res=max(res,t[x]);
	return res;
}
inline void upd(int x,int v){
	for(;x<=n*k;x+=x&(-x))t[x]=max(t[x],v);
}
int main(){
	cin>>n>>k;
	For(i,1,n*k)cin>>a[i];
	For(i,1,n*k){int x;cin>>x;p[x][++cnt[x]]=i;}
	For(i,1,n*k){
		for(int j=k;j>=1;j--){
			int res=qry(p[a[i]][j]-1)+1;
			ans=max(ans,res);
			upd(p[a[i]][j],res);
		}
	}
	cout<<ans;
	return 0;
}

27

给定两个长度均为 \(n\) 的数组 \(A_i\)\(B_i\) 。有 \(m\) 次操作,每次操作将 \(A_x\) 修改为 \(y\) 或者将 \(B_x\) 修改为 \(y\) 或者求出

\[\max_{x\le i<j\le y} \{A_i+A_j-\min_{i < k < j}(B_k)\} \]

\(1\le n,m\le 5\times 10^5\)

在下文中,不妨记一段区间 \([x,y]\) 上的答案为 \(\Psi(x,y)\) ,也就是

\[\Psi(x,y)=\max_{x\le a<b<c\le y} \{A_a-B_b+A_c\} \]

那么对于每次询问,问题转化为了计算 \(\Psi(x_i,y_i)\) 的值。考虑使用线段树解决本题。特别地,下文中记 \(\Psi(\omega)\) 表示线段树上节点 \(\omega\) 对应的区间求得的 \(\Psi\) 值。

非常自然的想法是,每个节点都要维护它所代表的这段区间的最优解。也就是说,假如一个节点 \(\omega\) 维护了区间 \([l,r]\) ,那么它就要存储 \(\Psi(l,r)\) 。问题在于,怎么从它的两个子节点 \(\alpha,\beta\)\(\Psi\) 值更新到它本身。

  • 首先考虑最优的 \(a,b,c\) 在同一个儿子中的情况。显然,这样对答案的更新就是 \(\max\{\Psi(\alpha),\Psi(\beta)\}\)

  • 然后考虑 \(a\)\(\alpha\) 中,而 \(c\)\(\beta\) 中的情况。考虑第二张照片在哪。可以发现,假如第二张照片在 \(\alpha\) 中,那么 \(c\) 的最优解必定是 \(\beta\)\(A\) 的值最大的那个元素。同理,假如第二张照片在 \(\beta\) 中,那么 \(a\) 的最优解必定是 \(\alpha\)\(A\) 的值最大的那个元素。而维护 每个节点对应区间上的 \(\max \{A_i\}\) 是相当容易的。

    于是,问题转化为了在一个节点维护的区间中,这样的两个值:

    \[\max_{l\le a<b\le r}\{A_a-B_b\} \]

    \[\max_{l\le b<c\le r}\{A_c-B_b\} \]

    不妨分别记为 \(P(\omega)\)\(Q(\omega)\) 。考虑怎么从它的左右儿子上转移过来。

    对于 \(P(\omega)\) ,其最优的 \(a,b\) 又可以分为如下三类:

    • \(a,b\) 都在左儿子 \(\alpha\) 中。这部分的贡献为 \(P(\alpha)\)

    • \(a,b\) 都在右儿子 \(\beta\) 中。这部分的贡献为 \(P(\beta)\)

    • \(a\) 在左儿子中,而 \(b\) 在右儿子中。显然这部分贡献应该是左儿子中 \(A\) 的最大值减去右儿子中 \(B\) 的最小值。不妨把一个节点维护的区间上 \(A_i\) 的最大值记为 \(X(\omega)\) ,而 \(B_i\) 的最小值记为 \(Y(\omega)\) ,那么有:

    \[P(\omega)=\max\{P(\alpha),P(\beta),X(\alpha)-Y(\beta)\} \]

    同理,我们可以得到 \(Q(\omega)\) 的维护方法。

最终,我们能得到所有东西的转移方程式:

\[\begin{gathered} X(\omega)=\max\{X(\alpha),X(\beta)\},Y(\omega)=\min\{Y(\alpha),Y(\beta)\} \cr \begin{aligned} P(\omega) &=\max\{P(\alpha),P(\beta),X(\alpha)-Y(\beta)\} \cr Q(\omega) &=\max\{Q(\alpha),Q(\beta),X(\beta)-Y(\alpha)\} \cr \end{aligned} \cr \Psi(\omega)=\max\{\Psi(\alpha),\Psi(\beta),P(\alpha)+X(\beta),X(\alpha)+Q(\beta)\} \end{gathered}\]

对于查询操作,我们只要把涉及到的区间从左往右一个一个合并起来,再查询它的 \(\Psi\) 值就行了。具体可以见代码。
遇到这种题目一定要想到在线段数维护区间上分类讨论,简化所求的东西。一般分为同段,跨段两种讨论。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m;
const int N=5e5+10;
int a[N],b[N];
struct node{
	int x,y,p,q,w; 
}t[N*4];
void pushup(int u){
	node ls=t[u*2],rs=t[u*2+1];
	t[u].x=max(ls.x,rs.x);
	t[u].y=min(ls.y,rs.y);
	t[u].p=max(max(ls.p,rs.p),ls.x-rs.y);
	t[u].q=max(max(ls.q,rs.q),rs.x-ls.y);
	t[u].w=max(max(ls.w,rs.w),max(ls.p+rs.x,ls.x+rs.q));
}
void build(int u,int l,int r){
	if(l==r){
		t[u].x=a[l];t[u].y=b[l];
		t[u].p=t[u].q=t[u].w=-1e9;
		return;
	}
	int m=(l+r)>>1;
	build(u*2,l,m);
	build(u*2+1,m+1,r);
	pushup(u);
}
//点修 
void upd(int u,int L,int R,int x,int k){//a
	if(L==R){t[u].x=k;return;}
	int m=(L+R)>>1;
	if(x<=m)upd(u*2,L,m,x,k);
	else upd(u*2+1,m+1,R,x,k);
	pushup(u);
}
void upd2(int u,int L,int R,int x,int k){//b
	if(L==R){t[u].y=k;return;}
	int m=(L+R)>>1;
	if(x<=m)upd2(u*2,L,m,x,k);
	else upd2(u*2+1,m+1,R,x,k);
	pushup(u);
}
node operator + (node ls,node rs){
	node u;
	u.x=max(ls.x,rs.x);		
	u.y=min(ls.y,rs.y);
	u.p=max(ls.p,max(rs.p,ls.x-rs.y));		
	u.q=max(ls.q,max(rs.q,rs.x-ls.y));
	u.w=max(ls.w,max(rs.w,max(ls.p+rs.x,ls.x+rs.q)));
	return u;
}
node qry(int u,int L,int R,int l,int r){
	if(l<=L&&R<=r)return t[u];
	int m=(L+R)>>1;
	if(r<=m)return qry(u*2,L,m,l,r);
	if(l>m)return qry(u*2+1,m+1,R,l,r);
	return qry(u*2,L,m,l,r)+qry(u*2+1,m+1,R,l,r);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	For(i,1,n)cin>>a[i];
	For(i,1,n)cin>>b[i];
	build(1,1,n);
	while(m--){
		int op,x,y;
		cin>>op>>x>>y;
		if(op==1)upd(1,1,n,x,y);
		else if(op==2)upd2(1,1,n,x,y);
		else cout<<qry(1,1,n,x,y).w<<"\n";
	}
	return 0;
}

28

有一个 \(n\times m\) 的矩阵 \(A\)\(A\) 中每个元素都是 \([0,10^5]\) 中的整数。且形式化地,有

\[E_{i,j}=\sum_{1\le x\le n}\sum_{1\le y\le m}(|x-i|+|y-j|)A_{x,y} \]

现在要用矩阵 \(E\) 还原出一个符合上述条件的整数矩阵 \(A\)。不过还原出的 \(A\) 中的每个元素均属于 \([0,10^{16}]\) 即可。

要求单次时间复杂度为 \(O(nm)\)


怎么这么牛的一道题。

设:

\[C_i=\sum_{j=1}^{m}A_{i,j} \]

\[R_i=\sum_{i=1}^{n}A_{i,j} \]

\[E_{i,j}=\sum_{1\le x\le n}\sum_{1\le y\le m}(|x-i|+|y-j|)A_{x,y} \]

由这个式子,可以注意到:

\[E_{i,j}=\sum_{x=1}^{n}\left |x-i \right |\times C_x+\sum_{x=1}^{m}\left |x-j \right |\times R_x \]

进一步的有:

\[E_{i-1,j}+E_{i+1,j}-2\times E_{i,j}=2\times C_i \]

其中 \(2 \le i \le n-1\)

同理可以解出 \(R_j\)

也就是说,确定不了的只剩下 \(C_1,C_n,R_1,R_m\)

考虑贪心,我们依次考虑每个 \(A_{i,j}\),直接将其为 \(\min(R_i,C_i)\),然后把 \(R_i\)\(C_i\) 同时减去 \(A_{i,j}\),一直接着做下去。那么只要有解,这样做就一定能构造出一个 \(A_{i,j}\) 为非负整数的解。

于是这道神仙题就做完了(吗?)。

注意这里需要特判 \(n=1\)\(m=1\) 的情况!这两个需要特判换一个解法!

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m;
void solve(){
	cin>>n>>m;
	vector<vector<int> >E(n+1),A(n+1);
	vector<int>C(n+1),R(m+1);
	For(i,1,n){
		E[i].resize(m+1);
		A[i].resize(m+1);
	}
	For(i,1,n)For(j,1,m)cin>>E[i][j];
	For(i,2,n-1)C[i]=(E[i-1][1]+E[i+1][1]-2*E[i][1])/2;
	For(i,2,m-1)R[i]=(E[1][i-1]+E[1][i+1]-2*E[1][i])/2;
	if(n==1&&m==1){cout<<E[1][1]<<"\n";return ;}
	if(n==1){
		R[1]=E[1][m];
		For(i,2,m-1)R[1]-=(m-i)*R[i];
		R[m]=E[1][1];
		For(i,2,m-1)R[m]-=(i-1)*R[i];
		R[1]/=m-1,R[m]/=m-1;
		For(i,1,m)cout<<R[i]<<" ";
		cout<<"\n";
		return;
	}
	if(m==1){
		C[1]=E[n][1];
		For(i,2,n-1)C[1]-=(n-i)*C[i];
		C[n]=E[1][1];
		For(i,2,n-1)C[n]-=(i-1)*C[i];
		C[1]/=n-1,C[n]/=n-1;
		For(i,1,n)cout<<C[i]<<"\n";
		return;
	}
	int X=E[1][1];
	int Y=E[1][m];
	int Z=E[n][1];
	For(i,2,n-1){
		X-=(i-1)*C[i];
		Y-=(i-1)*C[i];
		Z-=(n-i)*C[i];
	}
	For(i,2,m-1){
		X-=(i-1)*R[i];
		Y-=(m-i)*R[i];
		Z-=(i-1)*R[i];
	}
	int W=0;
	For(i,2,m-1)W+=R[i];
	For(i,2,n-1)W-=C[i];
	W=W*(n-1)*(m-1);
	W-=(m-1)*Z-(n+m-2)*X-(n-1)*Y;
	W/=n-1,W/=2*n+2*m-4;
	C[n]=W;
	C[1]=(Z-X)/(n-1)+C[n];
	R[1]=(Y-(n-1)*C[n])/(m-1);
	R[m]=(X-(n-1)*C[n])/(m-1);
	For(i,1,n){
		For(j,1,m){
			A[i][j]=min(C[i],R[j]);
			C[i]-=A[i][j];
			R[j]-=A[i][j];
		}
	}
	For(i,1,n){
		For(j,1,m){
			cout<<A[i][j]<<" ";
		}
		cout<<"\n";	
	}
} 
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int T;cin>>T;
	while(T--)solve();
	return 0;
}

29

长为 \(n\) 的序列 \(a\) 满足\(0\le a_i < 2^m\) 且序列中的数字两两不同。约定 \(F(x)\)\(\forall a_i, a_i\rightarrow a_i \oplus x\) 后序列逆序对的个数。

现在需要找到第 \(k\) 小的 \(x\)。称 \(x_i<x_j\),当且仅当 \(F(x_i)<F(x_j)\),或者 \(F(x_i)=F(x_j)\)\(x_i<x_j\)

对于 \(100\%\) 的数据,\(1\le T\le 10\)\(1\le n,\sum n\le 10^6\)\(1\le m\le 30\)\(0\le a_i < 2^{m}\)\(1\le k\le 2^m\)。保证 \(a_i\) 互不相同。

异或后比大小可以拆位考虑一下。令两数最高不同的二进制位为 \(L\),则两数大小关系改变当且仅当 \(x\) 的第 \(L\) 位为 \(1\)

这是因为前 \(L-1\) 位相同,异或同一个数字没影响,因为第 \(L\) 位不管 \(x\) 数字是什么,两个数这一位必定不相同,所以后 \(n-L\) 位的值对大小没影响。

那么易得出 \(x\) 这一位为 \(1\) 可以改变大小关系。

\(0 \oplus 1=1,1\oplus 1=0;0 \oplus 0=0,1\oplus 0=1\)

因此每个二进制位是独立的。考虑求出初始逆序对个数,以及每个二进制位异或上 \(1\) 时逆序对的改变量。
前者可以通过归并排序或树状数组轻松求出。

逆序对的改变量指的是:把前面二进制位相同的放一组,每组求逆序对。

对于后者,从高到低枚举二进制位,对这一位不同的数求逆序对(只有 \(0/1\)),然后根据这一位将序列分为两个子序列,对两个序列分别往下做。

那么这部分时间复杂度为 \(O(nm)\)

那么接下来问题就变为:有 \(m\) 个数,从中选出若干个数并求和,要求找到第 \(k\) 小的和。先把 \(m\) 个数分成两份,每份 \(\frac{m}{2}\) 个数,然后对每份求出所有选法的和,并排序。时间复杂度 \(O(m2^{\frac{m}{2}})\)

这样就变成从两个序列中各选一个进行组合。然后二分答案 \(s\),二分时计算出有多少种选法的和小于等于 \(s\)。这里可以用双指针在两个序列上扫。这部分时间复杂度为 \(O(2^{\frac{m}{2}}\log n)\)

30

\(n\) 两序列 \(x,y\)。可以互换若干 \(x_i,y_i\)。要求最终的 \(x,y\) 序列中各自不含重复数字。最少换几次。\(n\le 5\times 10^4,x,y\le 10^5\)

如果两个相同的数在不在同一行,给两个数所在的列的编号连一条边权为 \(0\) 的边,否则连一条边权为 \(1\)的边。

给每个联通块的点黑白染色(\(1\) 边连接的点颜色相反,\(0\) 边连接的点颜色相同),答案每次加上每个联通块黑点和白点个数的最小值。

https://www.luogu.com.cn/article/rfudqj2s

题解有一点不清楚,这里感谢群内的 VainSylphid 大佬解答。以下是原话:

你发现 \(1\) 边相当于,我们希望这两列中恰有一列做交换。\(0\) 边相当于,如果这两列中一列交换了,另一列也要交换。

那你按照那个题解的方式染色,就相当于同一个颜色的必须全部交换或者全部不交换。

并且黑色和白色只有一种颜色全部交换,另一种全部不交换。

那取较小值就是显然的了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define For(i,l,r) for(int i=l;i<=r;i++)
int n;
const int N=1e5+10;
int a[N],b[N];
int r[N],l[N];
#define mp make_pair
int col[N];
int ans;
bool vis[N];
vector<pair<int,int> >G[N];
int B,W;
void dfs(int u){
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i].first;
		int w=G[u][i].second; 
		if(!vis[v]){
			vis[v]=1;
			if(w==0){
				col[v]=col[u];
				if(col[v]==1){
					B++;
				}
				else W++;
				dfs(v);
			}
			else{
				if(col[u]==1){
					col[v]=2;
					W++;
				}
				else{
					col[v]=1;
					B++;
				}
				dfs(v);
			}
		}
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
  	cin>>n;
  	For(i,1,n){
  		cin>>a[i];
  		int row=r[a[i]];
  		int line=l[a[i]];
  		if(row){
  			G[line].push_back(mp(i,1));
  			G[i].push_back(mp(line,1));
		}
		else{
			r[a[i]]=1;
			l[a[i]]=i;
		}
	}
	For(i,1,n){
		cin>>b[i];
		int row=r[b[i]];
  		int line=l[b[i]];
  		if(line){
  			if(row==1){
  				G[line].push_back(mp(i,0));
  				G[i].push_back(mp(line,0));
			}
			else{
				G[line].push_back(mp(i,1));
  				G[i].push_back(mp(line,1));
			}
		}
		else{
			r[b[i]]=2;
			l[b[i]]=i;
		}
	}
	For(i,1,n){
		B=0,W=0;
		if(!vis[i]){
			vis[i]=1;
			col[i]=1;
			B++;
			dfs(i);
			ans+=min(B,W);
		}
	}
	cout<<ans;
  	return 0;
}

31

题目

简化版题目为 LeetCode 253,以下是简化版本的解法。

每支乐队的演唱会时间不能相交,等价于“为每个区间分配一个颜色,且相交的区间颜色不同”。我们需要最少的颜色数,这就是区间图的最小着色数。

对于任意区间集合,最小着色数 \(=\) 区间集合在任意时刻的最大重叠次数

步骤如下。备注:不管区间有无无包含关系,这样的算法都是正确的。因为考虑堆内的元素如果现在能用之后也能用,现在用掉一定不劣。而且不会因为排序对后面产生不同影响,因为因为左端点相同的都放一块了,扫到下一个的时候早就已经全部入队了。

  • 首先,对区间按左端点升序排序。

  • 然后,用小根堆优先队列,计算最大重叠次数。

    • 对于当前区间 \([l_i, r_i]\),先检查堆顶元素(最早结束的演唱会结束时间 \(top_r\))。
    • \(top_r < l_i\),即最早结束的演唱会在当前演唱会开始前已结束,无重叠。则弹出堆顶元素。该乐队可承接当前演唱会,无需新增乐队。
    • 将当前区间的右端点 \(r_i\) 加入堆,表示新增一场正在进行的演唱会。若堆大小增加,则可能需要更多乐队。
    • 堆的大小即当前同时进行的演唱会数量,也就是此时需要的乐队数,取最大值即为答案。

说回本题。

考虑确定时间段区间怎么求乐队数量。从左往右依次贪是对的,现在能继承⼀定是继承更优,记录⼀下现在还在堆里的右端点,如果最左边的那个 \(\le l\),弹出并把 \(r\) 加⼊,代价为 \(0\)。否则直接把 \(r\) 加⼊,代价为 \(1\)

考虑每个时间段的覆盖次数 \(t_i\),根据上述分析答案就是 \(\max t_i\)

直接 dp \(t_i\),令 \(t_0=t_{n+1}=0\)。注意到序列 \(t\) 合法当且仅当 \(\left | t_{i+1}-t_i \right |\le 1\)

还需要考虑 \(t_i\) 对应多少个区间集合。\(t_i=t_{i+1}\) 考虑被 \(i\) 覆盖的每个区间,要么不变;要么左端点最左边的那个在 \(i\) 结束,同时在 \(i+1\) 新开一个区间。一共 \(2\) 种方案。

\(k=\sum_i[t_i \ne 0 \ \land \ t_i=t_{i+1}]\),那么贡献就是 \(2^k\)。这⾥的⽅案对其他位置的影响是独⽴的,因为只需要考虑相邻位置的映射,所以贡献是 \(2\)。其余情况映射都是唯⼀的。

因为合法的区间集合与满足 \(|t_{i+1}-t_i| \leq 1\) 的覆盖次数序列 \(t\) 是一一对应关系,且序列中相邻位置 \(t_i\)\(t_{i+1}\) 的关系直接决定了区间在 \(i\)\(i+1\) 处的增减(对应区间开始或结束),仅需通过相邻位置的 \(t\) 就能唯一确定区间集合的结构,进而计算其贡献,无需考虑非相邻位置。

暴力 dp 即可,是 \(O(n^3)\) 的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
const int p=998244353;
int n;
/*动态规划计算当最多使用
k支乐队时的合法集合数量
冲突的定义:
两个演唱会的时间区间存在重叠
包括端点处重叠*/ 
ll dp(int k){
    /*f[i]表示考虑完所有时间段后,
	被选中的演唱会中最大冲突深度为i的合法集合数量
	f[i]统计范围是所有选中的演唱会彼此完全不相交的集合
	例如 [1,2]、[3,4]、[5,6] 这样的时间段,
	任意两个都不重叠,同时进行的演唱会最多只有 1 个*/
    vector<ll>f(n+1),g(n+1);
    f[0]=1;
    For(i,0,n-1){
        fill(g.begin(),g.end(),0);
        For(j,0,k){
            /*情况1:不选当前时间段 
			或选了但不增加冲突深度
			(j=0时无冲突*/
            g[j]=(g[j]+f[j]*(j?2:1))%p;
            //情况2:从深度j+1转移到j
            if(j+1<=k){
                g[j]=(g[j]+f[j+1])%p;
            }
            //情况3:从深度j-1转移到j
            if(j-1>=0){
                g[j]=(g[j]+f[j-1])%p;
            }
        }
        swap(f,g);
		//新状态变成旧状态,旧状态直接等覆盖 
    }
    //返回最多使用k支乐队的合法集合总数
    return (f[0]+f[1])%p;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n;
    For(k,1,n)cout<<dp(k)<<" ";
    return 0;
}

32

QQ20251030-194434

数学题,推导如下。

\(A\) 赢下 \(P\) 局,\(B\)\(Q\) 局,可以先乘以一个组合数 \(\binom{P+Q}{P}\)。然后注意到,不管每局谁赢了,都是 \(+1\)。也就是说谁输谁赢对于一句之后的等级分总和并没有影响。那么我们可知答案:

\[[ R_m\times(R_m+1)\cdots\times(R_m+P-1)]\ (A连赢P局) \]

\[\times \]

\[[R_h\times(R_h+1)\cdots\times(R_h+Q-1)]\ (B连赢Q局) \]

\[{\div} \]

\[(R_m+R_h)\times(R_m+R_h+1)\times\cdots\times(R_m+R_h+P+Q-1) \]

化简完就是 $$ \binom{P+Q}{P} \frac{(R_m+R_h-1)!(R_m+P-1)!(R_h+Q-1)!}{(R_m+R_h+P+Q-1)!(R_m-1)!(R_h-1)!}$$

33

QQ20251030-203846

应该是 $2\times\left \lfloor \frac{n}{2} \right \rfloor ! $,烛火笔误了。

34

QQ20251030-195709

用换根 DP,两次 DFS 解决:

第一次自底向上:算每个节点作为临时根时,其子树内所有游客到它的最大时间,总代价,同时记每个节点的最大时间和次大时间。

第二次自顶向下:把根从父节点移到子节点,算非子树部分的游客到新根的最大时间和代价,用第一次记的最大/次大排除重复计算。

算次大是因为父节点 \(u\) 的最大时间是子节点 \(v\) 贡献的,即 \(v\) 的子树时间 + \(u\rightarrow v\) 边的时间。当我们把根从 \(u\) 换到 \(v\) 时,这时候不能再用 \(u\) 的最大时间,因为包含了 \(v\) 的贡献,只能用 \(u\) 的次大时间(来自其他子树,和 \(v\) 无关)。

最后对每个节点,检查子树和非子树的最大时间是否 \(\le D\),选代价最小的。

35

给定一个长度为 \(n\) 的排列 \(a\),每次可以选择 \(a_i < a_{i+1}\),然后从数组中移除 \(a_i\)\(a_{i+1}\)。问能否只剩下一个数字?

充要条件是 \(a_1<a_n\)

尽可能保留 \(a_1\),因为我们希望前面的数字尽可能小,你要是删掉它,肯定得用一个大一点的数字,那你换成一个更大的数字,怎么说都是不优的。

同理 \(a_n\) 也要尽可能保留。

我们考虑 \(a_1<a_2\) 时删掉 \(a_2\),持续这么做,直到 \(a_2>a_1\),同理尝试删除 \(a_3\),一直删一直删,但是保留 \(a_n\) 不动。

最后会删成一个单调上升的数列,后面跟着 \(a_n\),显然,想要删成一个数字的条件是 \(a_1<a_n\),这个结论直接用就可以了。

36

QQ20251030-214301

区间最小值必然是其中某个元素 \(x\),且要满足区间内所有元素都是 \(x\) 的倍数,否则 gcd 会小于 \(x\)\(f_x\) 是最小值为 \(x\) 的区间的最大可能长度。\(cnt_x\) 是数组中值等于 \(x\) 的元素个数。

转移方程 \(f_x=max{(f_{i\times x})}+cnt_x\)。因为可以重排,所以当成选数做就行了。答案是区间长度乘以区间最小值 \(xf_x\)

对任意一个数 \(y\),它只会被自己的约数 \(x\) 访问到。而一个数的约数个数是 \(\log\) 级的,所以复杂度可过。

37

QQ20251030-214311

38

QQ20251030-214322

posted @ 2025-10-28 18:41  Accept_Reality  阅读(26)  评论(0)    收藏  举报