Educational Codeforces Round 152 A~F
题意:有
while(t--){
int a,b,c;cin>>a>>b>>c;
cout<<min(a+a-1,b+c+b+c+1)<<"\n";
}
题意:打怪兽,每次选择血量最高的怪兽,一次打掉
先把所有怪兽的血量模到
题意:给定一个01序列,复制
考虑将
显然,
注意原序列也算,所以要特判一下对序列不会有改变的情况。也即改的一段1/0的连续段,亦或者一段0+一段1
while(t--){
int n,k;cin>>n>>k;
for(int i=1;i<=n;i++){
char x;cin>>x;
a[i]=x-'0';
}
int c=0,d=0;a[0]=-1;
for(int i=1;i<=n;i++){
if(a[i]!=a[i-1])cnt[i]=++c;
else cnt[i]=cnt[i-1];
s[i]=a[i]+s[i-1];
if(a[i])pre[i]=i;
else pre[i]=pre[i-1];
}
nxt[n+1]=n+1;
for(int i=n;i;--i){
if(!a[i])nxt[i]=i;
else nxt[i]=nxt[i+1];
}
int tot=0;
while(k--){
int l,r;cin>>l>>r;
if(cnt[r]==cnt[l]||cnt[r]-cnt[l]==1&&a[l]==0)l=0,r=0;
else l=pre[l-1]+1,r=nxt[r+1]-1;
w[++tot]=(node){l,r};
}
sort(w+1,w+tot+1);
tot=unique(w+1,w+tot+1)-w-1;
cout<<tot<<"\n";
}
赛时眼瞎,打掉了一个加一,然后没了。
这种无后效性又具有明显可合并性的最优化问题,多半是DP。
注意到对于一个非零连续段而言,它实际可以扩展的取决于该连续段里最大的数(2就能扩展两边,1只能扩展一边。
我们将它合并为一个数,然后考虑动态规划。注意到这里与下一个数是不是已经被更新有关,故设
当当前数为
当当前数为
当前数为2时,显然有
注意当
初始化:
尤其注意状态转移的时候要每一个状态都考虑到
有意思,学到了。
题意:给定一个
解决此类关于最大最小值的区间统计问题,一般套路是定最值。其目的是简化计算。
解决区间统计的问题的一般套路是定一端。其目的是减少重复计算。
下面我们就这两种思想解决这个问题。
解法1-最大值分治
我们考虑确定最大值去解决这个问题(最小值是同理的)
首先考虑当最大值为
朴素的想法是:令
然后我们惊奇地发现,
不难写出以下递归函数:
void solve(int l,int r){
if(l>=r)return ;
int p=-1;
for(int i=l;i<=r;i++){
if(p==-1||a[p]<a[i])p=i;
}
solve(l,p-1);solve(p+1,r);
f[p]=a[p];
for(int i=p-1;i>=l;--i)f[i]=min(f[i+1],a[i]);
for(int i=p+1;i<=r;++i)f[i]=min(f[i-1],a[i]);
int j=p;
for(int i=p-1;i>=l;--i){
while(j<=r&&f[j]>f[i])++j;
ans+=j-p;
}
}
是的,这个算法在随机数据下表现良好,有着接近
但,这个算法很容易被卡。因为它会扫遍整个区间,这会使得复杂度爆表(划分不均匀)我们必须要规避这种风险。
How?启发式合并(其实应该叫启发式分裂?)!这样,注意到我们算法里,
那么我们考虑用单次时间换整体时间。显然,别闹,这是OI,啥扯淡都会发生。
顺带着,我们考虑选择左右较短的那个区间进行扫描处理:若选择了右边区间,可以用容斥的方法解决问题。
那么,考虑枚举较短的区间,维护扫过的那段的最小值,这时候怎么确定答案范围呢?很简单,用ST表维护区间最值,然后二分这个位置即可。不要用其他数据结构!ST表查询
另外,注意到递归层数1e6,为了防爆,推荐各位壮士换成队列的广搜吧。
#include<iostream>
#include<cstdio>
#include<queue>
#define ll long long
using namespace std;
#define N 1050500
#define pr pair<int,int>
#define mk make_pair
#define l first
#define r second
queue<pr >q;
int a[N],n,f[N],mx[N][22],mn[N][22];
int lg[N];
ll ans;
void init_st(){
// cout<<"init_st:\n";
for(int i=1;i<=n;i++)mn[i][0]=a[i],mx[i][0]=i;
for(int i=2;i<=n;i++)lg[i]=lg[i/2]+1;
for(int i=1;i<=20;i++){
for(int j=1;j+(1<<i)-1<=n;j++){
mn[j][i]=min(mn[j][i-1],mn[j+(1<<(i-1))][i-1]);
mx[j][i]=a[mx[j][i-1]]>a[mx[j+(1<<(i-1))][i-1]]?mx[j][i-1]:mx[j+(1<<(i-1))][i-1];
}
}
}
int find_n(int l,int r){
int k=lg[r-l+1];
return min(mn[l][k],mn[r-(1<<k)+1][k]);
}
int find_x(int l,int r){
int k=lg[r-l+1];
return a[mx[l][k]]>a[mx[r-(1<<k)+1][k]]?mx[l][k]:mx[r-(1<<k)+1][k];
}
#define int long long
void solve(int l,int r){
if(l>=r)return ;
int p=find_x(l,r);
q.push(mk(l,p-1));q.push(mk(p+1,r));
// cout<<l<<" "<<r<<" "<<p<<"\n";
int s=ans;
if(p-l<=r-p){
int mn=a[p];
for(int i=p-1;i>=l;--i){
mn=min(mn,1ll*a[i]);
int L=p,R=r+1;
while(L<R){
int mid=L+R>>1;
if(find_n(p,mid)<=mn)R=mid;
else L=mid+1;
}
if(r==n&&find_n(p,r)>mn)L=n+1;
ans+=L-p;
}
}
else {
ans+=(r-p+1)*(p-l);int mn=a[p];
for(int i=p+1;i<=r;++i){
mn=min(mn,1ll*a[i]);
int L=l-1,R=p;
while(L<R){
int mid=L+R+1>>1;
if(find_n(mid,p)<mn)L=mid;
else R=mid-1;
}
if(l==1&&find_n(1,p)>mn)L=0;
ans-=p-L-1ll;
}
}
}
#undef int
void work(){
q.push(mk(1,n));
while(q.size()){
solve(q.front().l,q.front().r);
q.pop();
}
}
void read(int &x){
x=0;char ch=getchar();
while(ch>'9'||ch<'0')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
signed main(){
// freopen("data.in","r",stdin);
ios::sync_with_stdio(false);
read(n);
for(int i=1;i<=n;i++)read(a[i]);a[n+1]=0x3f3f3f3f;
init_st();
work();
cout<<ans<<"\n";
}
用启发式合并来分治,复杂度
解法2-单调栈+线段树
我们考虑确定右端点(左端点同理),统计当区间右端点为
容易发现,对于
为整个序列的最大值 为整个序列的最小值
其余情况的答案在
而怎么维护这个答案呢?妙点就出来了。考虑维护合法的区间左端点集合以及更换右端点的影响范围。设
a[0]=0x3f3f3f3f;s.push(0);
for(int i=1;i<=n;i++){
if(a[s.top()]>a[i]){
b[i]=s.top();s.push(i);continue;
}
while(a[s.top()]<a[i])s.pop();
b[i]=s.top();s.push(i);
}
while(!s.empty())s.pop();
a[0]=0;s.push(0);
for(int i=1;i<=n;i++){
if(a[s.top()]<a[i]){
c[i]=s.top();s.push(i);continue;
}
while(a[s.top()]>a[i])s.pop();
c[i]=s.top();s.push(i);
}
那么影响范围呢?当
这使得我们考虑维护答案序列
,将 全部改为 ,将 全部改为
最后我们统计全局和就可以了。
这一步可以用线段树维护。时间复杂度
解法1
暴力的想法是二分答案,然后将符合条件的
考虑优化这个做法。因为它的边数是
这里有异或的一个性质:
故,先排序,然后可以发现,若存在边
我们只需判断是否存在奇环即可。怎么搞?若存在边
若
四种情况。而这四种情况呢,很容易发现,无论哪一种,都必定存在至少三个数,互相异或后此位为0,而此位为
所以,我们只需要判一下是否
复杂度
解法2
一个很简朴的思路是:不断找剩下的最小异或对,将其分到两个集合。
划分两个集合的常见套路是二分图。
那么我们换一种角度,将这些最小异或对看作一条边,连成图,直到它不是二分图为止。
则当这张图连通时,二分图的划分就已经确定了,所以只要
再转化一下,就是在一张完全图,边
容易看出,这就是最小异或生成树问题。
故:我们求出最小异或生成树,然后黑白染色输出方案即可。
#include<bits/stdc++.h>
using namespace std;
#define N 500500
#define int long long
int num,p,ch[N<<4][2],l[N<<4],r[N<<4],n,b[N];
int head[N],ver[N],nxt[N],c[N],tot,id;
struct node{
int x,id;
bool operator<(const node b)const {
return x<b.x;
}
}a[N];
void ad(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void add(int u,int v){
ad(u,v);ad(v,u);
}
void insert(int x,int p,int id,int dep){
if(dep==-1)return ;
int s=(x>>dep)&1;
if(ch[p][s])r[ch[p][s]]=id;
else ch[p][s]=++num,l[num]=r[num]=id;
insert(x,ch[p][s],id,dep-1);
}
int query(int x,int p,int dep){
if(dep==-1){
id=l[p];return 0;
}
int s=(x>>dep)&1;
if(ch[p][s])return query(x,ch[p][s],dep-1);
return query(x,ch[p][s^1],dep-1)+(1<<dep);
}
void solve(int p,int dep){
if(!p||dep==-1)return ;
int x=ch[p][0],y=ch[p][1];
if(x&&y){
if(r[x]-l[x]+1>r[y]-l[y]+1)swap(x,y);
int ans=0x3f3f3f3f,u=-1,v=-1;
for(int i=l[x];i<=r[x];i++){
int k=query(a[i].x,y,dep-1);
if(ans>k)ans=k,u=i,v=id;
}
add(a[u].id,a[v].id);
}
solve(x,dep-1);solve(y,dep-1);
}
void dfs(int u,int now){
c[u]=now;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];if(c[v]!=-1)continue;
dfs(v,now^1);
}
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;num=1;for(int i=1;i<=n;i++)cin>>a[i].x,b[i]=a[i].x,a[i].id=i;
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)insert(a[i].x,1,i,30),c[i]=-1;
solve(1,30);dfs(1,0);
for(int i=1;i<=n;i++)cout<<c[i];
}
至于最小异或生成树的求法,请看下文。
时间复杂度:
完全图最小异或生成树
挺有意思的。
首先证明:一颗Trie树插入
理由:除插入第一个数外,其余每插入一颗树,Trie都会进行一次分叉,使得一个只有一个儿子的节点变为两个儿子,而新分出来的也只是一条链。
根据Kruskal的思想,我们找连接两个集合的权值最小的边进行合并两个集合,则我们在Trie上找这
由于Trie的性质,这些数在排完序后是连续的,故这两个集合也是连续的一段。
我们考虑枚举其中较短的一半(启发式分裂),并在另一颗子树里找与枚举的数异或值最小的数。最终找出来的一对,将其连上,便合并了两个集合,递归处理即可。
详细介绍+模板题见CF888G
#include<bits/stdc++.h>
using namespace std;
#define N 250500
#define int long long
int a[N],num,p,ch[N<<5][2],l[N<<5],r[N<<5],n;
void insert(int x,int p,int id,int dep){
if(dep==-1)return ;
int s=(x>>dep)&1;
if(ch[p][s])r[ch[p][s]]=id;
else ch[p][s]=++num,l[num]=r[num]=id;
insert(x,ch[p][s],id,dep-1);
}
int query(int x,int p,int dep){
if(dep==-1)return 0;
int s=(x>>dep)&1;
if(ch[p][s])return query(x,ch[p][s],dep-1);
return query(x,ch[p][s^1],dep-1)+(1<<dep);
}
int solve(int p,int dep){
if(!p||dep==-1)return 0;
int x=ch[p][0],y=ch[p][1];
if(x&&y){
if(r[x]-l[x]+1>r[y]-l[y]+1)swap(x,y);
int ans=0x3f3f3f3f;
for(int i=l[x];i<=r[x];i++){
ans=min(ans,query(a[i],y,dep-1));
}
return solve(x,dep-1)+solve(y,dep-1)+ans+(1<<dep);
}
if(x)return solve(x,dep-1);
return solve(y,dep-1);
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;num=1;for(int i=1;i<=n;i++)cin>>a[i];
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)insert(a[i],1,i,30);
cout<<solve(1,30);
}
总结
- 区间统计问题的两个套路
- 单调栈维护前面第一个大于/小于
的位置 - 二分图解决不在同一集合,分为两个集合问题
- 黑白染色后集合划分在图连通时就确定了
- 完全图最小异或生成树
- Tire树的有序性
- Trie树证明异或性质,最小异或对的必要条件是两个数在Trie的LCA是最大的——>用DFS序的思想可以得到相邻数的异或差最小
- 一颗插入
个节点(相异)的Trie,有且仅有 个节点有两个儿子。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!