目录
新月轩就餐
思路:
其实很容易想到是双指针或者双端队列。
我们设置一个type表示当前区间已经有了多少种厨师,同时还需要记录区间中每个元素出现的次数,然后比较棘手的是移动问题了,什么时候移动呢?
我们可以发现当区间当队头元素多余时候肯定就要移动了:
统计答案就是在type等于m的时候,只要统计l和r的位置就行了。
为什么这样的移动方式就一定可以让l和r落在最佳的答案区间上?
你可以反推:因为我们的l指向的元素一定是区间中没有多余的元素,那么如果它再右移,则区间非法;如果它不移动,也就是在l的左边,那么这个区间明显不是最佳区间。
#include <bits/stdc++.h>
using namespace std;
const int N=16+5,inf=0x3f3f3f3f;
deque<int>q;
int ans=inf;
int n,m,a[N],cnt[2001],l,r,type;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
if(!cnt[a[i]])type++;
cnt[a[i]]++;
q.push_back(i);//新元素进入队尾
while(!q.empty()&&cnt[a[q.front()]]>1){//队头多余时候就移动
cnt[a[q.front()]]--;q.pop_front();
}
if(type==m){//因为种类最多m,达到m时候就一直更新答案就行
if(q.size()<ans){
ans=q.size();l=q.front();r=q.back();
}
}
}
cout<<l<<" "<<r;
}
red and blue
蛮有意思的的一道题,最后要判断能否成为一种1~n的全排列,我最这样做的:
整个数组先排序一下。假设遍历到了i,那就判断前面b和r的个数,但是有想到了后面可能还会对前面的结果产生影响,所以就抛弃了这个想法。
然后就想:既然是一种排序,那么能不能先把这个排序确定下来呢,事实上是可以的。
做法就成了这样子:整个数组还是先排序,降序排序,然后把r放在b前面。(一会解释)
假如说现在变成了这样:
6 4 4 3 3 1(r r b r b r)
那么6可以变成6,4也可以变成5,4也可以变成4,3也可以变成3,3也可以变成2,1也可以变成1
这样就变成了6 5 4 3 2 1
但是我实际是升序来写的,因为更方便一点,还有一点就是要注意有时候可能光升序并不能判断出所有情况,还要降序再来一次,所以我reverse了一下。
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
struct node{
int x;char c;
}a[N];
int t,n;
bool cmp(node a,node b){
if(a.x!=b.x)return a.x<b.x;
else return a.c<b.c;
}
int main(){
cin>>t;
while(t--){
cin>>n;string s;
for(int i=1;i<=n;i++)cin>>a[i].x;
cin>>s;
for(int i=1;i<=n;i++)a[i].c=s[i-1];
sort(a+1,a+n+1,cmp);
int f1=0;
for(int i=1;i<=n;i++){
if(a[i].x==i)continue;
else if(a[i].x<i&&a[i].c=='R')continue;
else if(a[i].x>i&&a[i].c=='B')continue;
else {f1=1;break;}
}
reverse(a+1,a+1+n);
int f2=0;
for(int i=1;i<=n;i++){
if(a[i].x==i)continue;
else if(a[i].x<i&&a[i].c=='R')continue;
else if(a[i].x>i&&a[i].c=='B')continue;
else {f2=1;break;}
}
if(f1==0||f2==0)cout<<"YES\n";
else cout<<"NO\n";
}
return 0;
}
天梯赛
这个题肯定是要排序的(根据直觉),可以按照人数要求来降序排序,这样的话遍历到当前的人的要求时候,就不需要再考虑前面的人了。
所以我们先排序,然后一次遍历每个人,如果当前的人数要求满足就直接放进来,如果不满足就看情况来踢掉当前队伍中战力最低的人。这里要一直更新ans才能获取最大值,当然还需要一个优先级队列。
另外注意如果求的是最小或者最大,你没有必要知道答案在哪里,只需要把所有的情况都去一次最值即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5;
struct node{
ll A,B;
// bool operator>(const node &b)const {return A>b.A;}
}a[N];
bool operator>(const node &a,const node &b){
return a.A>b.A;
}
ll ans,n,sum;
priority_queue<node,vector<node>,greater<node> >q;
bool cmp(node a,node b){
return a.B>b.B;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i].A>>a[i].B;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++){
q.push(a[i]);
sum+=a[i].A;
while(q.size()>a[i].B){
sum-=q.top().A;
q.pop();
}
ans=max(ans,sum);
}
cout<<ans;
return 0;
return 0;
}
构造原理
这就是一个dfs的题,我们可以设置dfs(x,i,j)表示当前还剩多少个电子没有安排,同时遍历到<i,j>坐标了。我们来构造一下坐标系,方便处理
我们观察规律发现只要j不等于1,就要i++,j--,那么我们只需要判断j=1时候,下一个如何转移即可。
(1,1) ->(2,1)
(2,1) ->(2,2)
(3,1) ->(3,2)
(4,1) ->(3,3)
(5,1) ->(4,3)
(6,1) ->(4,4)
观察i和ii,jj
i ii jj
1 2 1
2 2 2
3 3 2
4 3 3
5 4 3
6 4 4
我们发现:ii=(i+1)/2+1 jj=i/2+1(也算是老朋友了,一定要特别熟悉它们)
然后就是要注意输出的时候不是按照构造的顺序,而是按照能层顺序,这就要我们提前打好表,然后按照能层来输出,那么dfs的功能就是按照构造顺序初始化每个能级的电子数。
顺便在说一下取模的问题:
如果你想把一个数映射到0~4,那么i%5 ; 如果你想映射到1~4,那么(i-1)%4+1
#include <bits/stdc++.h>
using namespace std;
char ch[10]="#spdf";
int n,sum[10][5],cnt[5]={0,2,6,10,14};
void f(int x,int i,int j){
sum[i][j]=min(cnt[j],x);//求每个能级的电子数
int y=x-cnt[j];
if(y<=0)return ;
if(j==1)j=i/2+1,i=(i+1)/2+1;
else i++,j--;
f(y,i,j);
}
int main(){
cin>>n;
f(n,1,1);
for(int i=1;;i++,cout<<'\n'){
for(int j=1;j<=4;j++){
if(!sum[i][1]) return 0;//这里一定要小心,当第一个都没有电子的时候,后面的能级更不用说了
if(sum[i][j]) cout<<i<<ch[j]<<sum[i][j]<<" ";
}
}
return 0;
}
异或和的或
一眼就是位运算的题,既然是求最小值,那么我们按位考虑的话,根据或运算的性质,我们尽可能使得m个区间的异或和的高位都是0,或者说必须都是0才使得最终ans的高位是0,从而ans就能尽可能的小。
① 第一个要解决的问题是如何快速求区间异或和,其实和前缀和一样的,我们直接求前缀异或和就行,然后求a[i]~a[j]异或和时等价于sum[j]^sum[i-1]。
sum[j]=1表示1~j的某个位有奇数个1,sum[j]=0表示1~j的某个位有偶数个1。
②然后是在处理每一位的时候我们不能去整体考虑如何划分m个段,因为这样需要一下子取出m个分割点进行遍历,当然是不行的,那么就只能分开考虑了。
我们单独考虑每个点的位置能不能被划分,如果可以的话,那么这就算是一种可能的分割点。
如何直接确定这个位置是一个可能的分割点呢?
当我们对这个点i进行分割的时候,如果区间1~i的对应位上1的个数是奇数,也就是sum[i]=1就绝对不行,即便你可以再次分割区间1~i,但是无论怎么分割一定有某一个区间的1的个数还是奇数个,那么就一定不是最优方案,所以我们对于所有sum[i]为0的点标记为可能的分割点。
③最后要解决的问题是低位和高位之间的关系,低位只需要遍历上个高位留下的可能的分割点,然后看看有哪些分割点在这个位上是奇数个1,就在此基础上再次剔除掉特定的非法分割点,如此循环,直到遍历完所有的位。
最后再捋一下思路:
首先从高位到低位开始遍历以每个位置为右端点的区间前缀异或和的对应位上个1的个数,如果这个位置在之前可能分割点,而现在1的个数又是偶数个,那么仍然是一个可能分割点。
统计完分割点后判断是否大于m,如果大于那么就有了分割的必要条件,但是还要注意整个区间上的1的个数必须偶数个,那么这个位就可以修改为0,当然还要对一些新的不合法的分割点再进行过滤;如果小于m或者整个区间上1的个数是奇数个,那么这个位一定是1,ans加上去就行,当然我们也不再需要进行标记过滤了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e+5;
ll n,m,a[N],ans;
bool vis[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i],a[i]^=a[i-1];
for(int i=62;i>=0;i--){
ll cnt=0;
for(int j=1;j<=n;j++){
if(!vis[j]&&!(a[j]&(1ll<<i))) cnt++;//j点处应该是可能的分割点
}
if(cnt>=m&&!(a[n]&(1ll<<i))){//应该大于m个分割点且长n的数组可以完整分割,缺一不可
for(int j=1;j<=n;j++)
if(a[j]&(1ll<<i))vis[j]=1;//能分割就标记
}
else ans|=(1ll<<i);//不能分割就直接加上去,当然就不要再标记了
}
cout<<ans<<'\n';
return 0;
}
任务分配
其实就是从后向前去分配每个任务,能装下就装,装不下放到前面的人身上。
那么要解决的问题是如何去确定总时间,因为我们分配任务的依据是总时间。
注意到总时间具有单调性,也就是如果当前的时间很大,大概率能完成分配,如果当前的时间很小,大概率完成不了分配,所以我们对时间进行二分就行。
二分的时候可以直接正向模拟,没有必要反向去做,反正都一样。
二分出来答案后就直接按照这个ans进行反向模拟,我们设置c[i][0]表示第i个人的起始任务c[i][1]表示第i个人的终止任务号。
#include <bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int t,k,a[N],b[N],c[N][2],l,r,mid,ans;
bool check(int p){
int cur=1;
memset(b,0,sizeof(b));
for(int i=1;i<=t;i++){
if(b[cur]+a[i]>p)cur++,b[cur]+=a[i];
else b[cur]+=a[i];
if(cur>k)return false;
else return true;
}
if(cur>k)return false;
else return true;
}
void print(int p){
int cur=k,temp=0;
for(int i=t;i>0;i--){
if(!c[cur][1]) c[cur][1]=i;//标记每个人的任务终点
if(temp+a[i]>p){
c[cur][0]=i+1;//标记每个人任务的起点
cur--;
temp=a[i];
c[cur][1]=i;
}
else temp+=a[i];
}
c[cur][0]=1;
for(int i=1;i<=k;i++){
if(c[i][0]==0) cout<<0<<" "<<0<<'\n';//此人被分配到任务
else cout<<c[i][0]<<" "<<c[i][1]<<'\n';//输出每人的任务起点和终点
}
}
int main(){
l=-1;cin>>t>>k;
for(int i=1;i<=t;i++){
cin>>a[i];
l=max(l,a[i]);r+=a[i];
}
while(l<=r){
mid=l+(r-l)/2;
if(check(mid)) r=mid-1,ans=mid;
else l=mid+1;
}
print(ans);
return 0;
}