我们仍未知道那天所看见的题的解法 - 2
P3620 [APIO/CTSC 2007]数据备份
【分析过程】
可以总结以下两点:
- 办公楼的排列为直线而非环形
- 选两个相邻的办公楼永远是最优的
先将 \(n\) 个数读入,然后算出 \(n-1\) 个间隔的长度,加入小根堆
如果我们只是一股脑的找当前能选的最小值,然后加进 \(ans\) 的话,肯定是不对的
例如下面这组数据:
5 2
0 2 12 14 15
首先进行预处理:
2 10 2 1
我们发现首先程序会选择 \(1\),然后两个二都不能选了,最后肯定选 \(10\),答案为 \(11\)
但是我们用脚指头都能算出来正确答案是 \(4\)
怎么办呢?
我们发现对于一个位置 \(i\)
- 要么选 \(i\)
- 要么不选 \(i\),而是选择 \(i-1\) 和 \(i+1\)
所以发现一种情况不是最优的之后我们要立即将其改变为另一种情况
考虑反悔贪心,对于一个位置 \(i\),选择它之后将其值变为左右两边的值的和减去这个点的值
这也就是差值,如果这个数被选择,那么就意味着反悔,即原来的决策不是最优的
设 \(v_i\) 表示 \(i\) 这个位置的值
原始: \(v_{i-1}\;\;\;\;v_i\;\;\;\;v_{i+1}\)
选择 \(v_i\): \(v_{i-1}\;\;\;\;v_{i-1}+v_{i+1}-v_i\;\;\;\;v_{i+1}\)
\(ans=v_i\)
发现不是最优情况: \(v_{i-1}\;\;\;\;v_{i-1}+v_{i+1}-v_i\;\;\;\;v_{i+1}\)
\(ans=v_i+v_{i-1}+v_{i+1}-v_i\)
即
\(ans=v_{i-1}+v_{i+1}\)
反悔完成
【代码实现】
#include<iostream>
#include<queue>
#define N 1000001
#define int long long
using namespace std;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
int n,m,rd[N],in[N],ans,will,doing;
struct node{
node *pre,*nxt;
int v,num;
};
node *place[N];
inline void del(int n){
if(n==-1) return;
place[n]->pre->nxt=place[n]->nxt;
place[n]->nxt->pre=place[n]->pre;
delete place[n];
place[n]=NULL;
}
signed main(){
cin>>n>>m>>rd[0];
for(int i=1;i<n;i++){
cin>>rd[i];
in[i]=rd[i]-rd[i-1];
q.push(make_pair(in[i],i));
node *nd=new node;
place[i]=nd;
nd->v=in[i];nd->num=i;
if(i!=1){
nd->pre=place[i-1];
place[i-1]->nxt=nd;
}
}
node *nd=new node;
nd->num=-1;nd->v=2147483647;
place[1]->pre=nd;
place[n-1]->nxt=nd;
while(1){
if(q.empty()) break;
will=q.top().second;
if(place[will]==NULL){
q.pop();
continue;
}
ans+=q.top().first;
doing+=1;
if(doing==m) break;
place[will]->v=place[will]->pre->v+place[will]->nxt->v-place[will]->v;
del(place[will]->pre->num);del(place[will]->nxt->num);
q.pop();
q.push(make_pair(place[will]->v,will));
}
cout<<ans;
}
CF911D Inversion Counting
【题意】
给定一个序列
每次操作翻转一个区间 \([l,r]\)
求每次翻转后整个序列逆序对的奇偶性
【分析过程】
首先看到翻转区间我们可以想到Splay可以胜任
看到逆序对可以想到归并排序
但是根据 (CF的题目都是思维题) 时间复杂度可以很轻易地判断出这显然是不行的
考虑这道题目让我们求什么
显然,我们并不需要求出逆序对的数目,而只需要求它的奇偶性
对于一个要被翻转的区间 \([l,r]\)
显然长度 \(len=r-l+1\)
其全部数对的数目是:\(num=\frac{len(len-1)}{2}\)
对于这个区间:
-
其全部的正序对在翻转后会变成逆序对
-
其全部的逆序对在翻转后会变成正序对
重点来了
如果 \(num\) 是一个偶数,分类讨论其逆序对的奇偶性的两种情况
-
奇数,那么其正序对的数目也是奇数,翻转后奇偶性不变
-
偶数,显然翻转后奇偶性不变
如果 \(num\) 是一个奇数,其逆序对的奇偶性也有两种情况
-
奇数,那么其正序对的数目是偶数,翻转后奇偶性改变
-
偶数,那么其正序对的数目是奇数,翻转后奇偶性改变
【总结】
如果 \(num\) 是偶数,那么不管怎样奇偶性都不变,如果是奇数,那么不管怎么样奇偶性都会改变
【代码实现】
#include<iostream>
#include<algorithm>
using namespace std;
int a[500001],r[500001],len,ans,e,L,R,t,num;
void msort(int s,int e){
if(s==e) return;
long long int mid=(s+e)/2;
msort(s,mid);msort(mid+1,e);
int i=s,j=mid+1,k=s;
while(i<=mid&&j<=e){
if(a[i]<=a[j]){
r[k]=a[i];
k+=1;i+=1;
}
else{
r[k]=a[j];
k+=1;j+=1;
ans+=mid-i+1;
}
}
while(i<=mid){
r[k]=a[i];
k+=1;i+=1;
}
while(j<=e){
r[k]=a[j];
k+=1;j+=1;
}
for(int i=s;i<=e;i++)
a[i]=r[i];
}
int main(){
cin>>num;
for(int i=1;i<=num;i++){
cin>>a[i];
}
msort(1,num);
if(ans%2==0) e=1;
cin>>t;
while(t--){
cin>>L>>R;
len=R-L+1;
if(len*(len-1)/2%2){
e=!e;
}
cout<<(e?"even\n":"odd\n");
}
}
为什么我的归并排序时间复杂度假了QAQ
CF631C Report
【分析过程】
先看数据范围:200000,排除 \(O(n^2)\)
然后仔细看题,我们会发现几个很有趣的地方:
- 对序列操作不是对 \([l,r]\) 这个区间操作,而是对 \([1,r]\) 操作
- 这个操作不是区间翻转,区间加法或其他操作,而是排序
容易发现当有两次操作
t1 r1
t2 r2
时
如果 \(r_2\geq r_1\)
那么显然第一次的操作是没有任何用的,因为不管第一次怎么排序,第二次都会将其覆盖掉
至此,我们可以轻而易举的发现这些操作可以用一个单调栈来维护
最后栈中就会是这样一个样子:
t1 r1
t2 r2
......
ti ri
其中 \(r_1>r_2>\dots>r_i\)
对于栈中的两个连续二元组 \((t_1,r_1),(t_2,r_2)\) 其中 \(r_1>r_2\)
我们按照 \(t_1\) 所对应的排序法则排序,然后 \((r_2,r_1]\) 的每个位置对应的数就确定了
因为直接排序复杂度较高,这里我们用两个变量 \(le,re\) 作为左右指针来使用
【代码实现】
#include<iostream>
#include<algorithm>
#define N 500001
using namespace std;
struct Node{
int t,r;
}stk[N];
int in[N],n,m,t,r,top,ans[N],le,re,now;
int main(){
ios::sync_with_stdio(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>in[i];
for(int i=1;i<=m;i++){
cin>>t>>r;
while(top&&stk[top].r<=r) top-=1;
stk[++top]={t,r};
}
re=stk[1].r+1;now=1;
sort(in+1,in+re);
for(int i=re-1;i>=1;i--){
if(i<=stk[now+1].r&&now+1<=top) now+=1;
if(stk[now].t==1)
ans[i]=in[--re];
else
ans[i]=in[++le];
}
for(int i=1;i<=stk[1].r;i++)
cout<<ans[i]<<" ";
for(int i=stk[1].r+1;i<=n;i++)
cout<<in[i]<<" ";
}
CF985E Pencils and Boxes
【分析过程】
开题,设 \(f[i]\) 表示前 \(i\) 个物品是否满足条件
则显然 \(f[0]=1\)
然后排序,使其满足单调性
然后 \(1\to n\) 遍历,对于一个位置 \(i\) 可以看看区间 \([1,i-k]\) 是否有值为一的位置 \(o\) 满足 \(|f_i-f_o|\leq d\)
朴素实现复杂度为 \(O(n^2)\),显然可以用单调队列优化到 \(O(n)\)
但是代码复杂度没有做到最优化
上面这个思路是扫到哪个更新哪个,接下来的思路是扫到哪个更新后面的
依然设 \(f[i]\) 表示前 \(i\) 个物品是否满足条件
则对于每个值为一的位置 \(i\) 我们可以更新 \([i+k,n]\)
扫描这个区间的时候只要值满足 \(|f_i-f_o|\leq d\) 就可以直接赋值为 \(1\)
【代码实现】
#include<iostream>
#include<algorithm>
#define N 1000001
using namespace std;
int n,k,d,now,v[N];
bool is[N]={1};
signed main(){
cin>>n>>k>>d;
for(int i=1;i<=n;i++) cin>>v[i];
sort(v+1,v+n+1);
for(int i=0;i<=n;i++){
if(is[i]){
now=max(now,i+k);
while(now<=n&&v[now]<=v[i+1]+d) is[now++]=1;
}
}
cout<<(is[n]?"YES":"NO");
}