A.暴力
题意:
给一个字符串 s s s,要求从该字符串中抽取出一个子字符串序列 a a a,剩下的为 b b b
要求 a a a的字典序最小
我们暴力遍历一遍,找到最小的字符,让他作为 a a a即可
#include<bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(0);
int T;cin>>T;
while (T--)
{
string s;
cin>>s;
int id = 0;
for (int i=0;i<s.size();++i)
if (s[i]<s[id])id=i;
cout<<s[id]<<" ";
for (int i=0;i<s.size();++i)if (i!=id)cout<<s[i];
cout<<endl;
}
}
\newpage
B.思维+找规律
题意:
给一个数组 a a a,不断进行变换。
记 c n t [ n u m ] cnt[num] cnt[num],为 n u m num num在数组 a a a中出现的次数。
每一轮变换 a [ i ] a[i] a[i]变为 c n t [ a [ i ] ] cnt[a[i]] cnt[a[i]]
每一轮变换结束后, c n t cnt cnt更新
给出 q q q次询问,格式为
x x x k k k
要求输出在第 k k k轮变换结束的时候 a [ x ] a[x] a[x]的值
找规律,实际上在草稿纸上完了几轮后,我们便发现在过了一定的轮数后所有的值都不再改变了
因此,我们可以暴力进行 2 n 2n 2n轮,离线查询
所有轮数高于 2 n 2n 2n轮的询问,我们都按照第 2 n 2n 2n轮的结果输出
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2100;
const int maxm = 101000;
int p[maxn],a[maxn];
array<int,4> ques[maxm];
int n,q;
int main()
{
ios::sync_with_stdio(0);
int T;cin>>T;
while (T--)
{
cin>>n;
for (int i=1;i<=n;++i)cin>>p[i];
cin>>q;
for (int i=1;i<=q;++i)
{
cin>>ques[i][0]>>ques[i][1];
ques[i][2]=i;
}
sort(ques+1,ques+1+q,[](array<int,4>ar1,array<int,4> ar2)
{
return ar1[1]<ar2[1];
});
int pl = 1;
while (pl<=q&&ques[pl][1]==0)
{
ques[pl][3]=p[ques[pl][0]];
++pl;
}
for (int _=1;_<=2*n;++_)
{
for (int i=1;i<=n;++i)
{
a[i]=0;
}
for (int i=1;i<=n;++i)a[p[i]]++;
for (int i=1;i<=n;++i)p[i]=a[p[i]];
while (pl<=q&&ques[pl][1]==_)
{
ques[pl][3] = p[ques[pl][0]];
++pl;
}
}
while (pl<=q)
{
ques[pl][3] = p[ques[pl][0]];
++pl;
}
sort(ques+1,ques+1+q,[](array<int,4>ar1, array<int,4>ar2)
{
return ar1[2]<ar2[2];
});
for (int i=1;i<=q;++i)cout<<ques[i][3]<<"\n";
}
}
\newpage
C.思维
题意:
给一个长为 n n n的数组 a a a,要求选择一个数 k k k
接下来可以进行有限次操作:
每次操作从 a a a中选择 k k k个数,减去他们的与操作和
问:是否能在有限次操作中将所有的数归零
要求求出所有能做到的 k k k
结论一: k k k一定是 n n n的因数
考虑到与操作的特殊性,我们对这些数归零时,一定是选择了 k k k个相等的数,然后将他们一起变成零
因此,一定是 k k k个 k k k个变成零
所以, k k k一定是 n n n的因数
扩展结论一,我们按照每一位来看。
k k k一定是第 i i i位中为 1 1 1的个数的因数
基本想法和结论一一样
因此,我们统计每一位的 1 1 1的个数,求他们的 g c d gcd gcd
然后输出该 g c d gcd gcd所有的因数即可
特别的,如果所有的数都为 0 0 0,那么答案应该为 1 1 1到 n n n
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5+100;
int a[33];
int n;
int main()
{
ios::sync_with_stdio(0);
int T;cin>>T;
while (T--)
{
cin>>n;
memset(a,0,sizeof(a));
for (int i=1,num;i<=n;++i)
{
cin>>num;
for (int j=0;j<=30;++j)
{
a[j]+=(num&1);
num>>=1;
}
}
int gd = 0;
for (int i=0;i<=30;++i)gd = __gcd(gd,a[i]);
if (gd==0)
{
for (int i=1;i<=n;++i)cout<<i<<" ";cout<<endl;
continue;
}
for (int i=1;i<=gd;++i)
{
if (gd%i==0)cout<<i<<" ";
}cout<<endl;
}
}
\newpage
D.图论
给一个两个长度为 n n n的数组 a , b a,b a,b,刚开始你再索引 n n n,你的目标是越过索引 1 1 1,跳出数组。
每次你可以跳 0 0 0到 a [ i ] a[i] a[i]米,换而言之,身处索引 i i i你可以跳到 [ i − a [ i ] , a [ i ] ] [i-a[i],a[i]] [i−a[i],a[i]]
但是,到从 i i i跳到 j j j时,你会后退 b [ j ] b[j] b[j]步,即最终落脚在 j − b [ j ] j-b[j] j−b[j]处
要求你求出最少需要多少步才能跳出
很容易看出,这似乎是一个最短路模型
对于向下划 b [ j ] b[j] b[j]步这个条件,我们直接将 i → j i\rightarrow j i→j改成 i → j − b [ j ] i\rightarrow j-b[j] i→j−b[j]即可
但是条件一中我们要从 i i i到 i , i − a [ i ] i,i-a[i] i,i−a[i]连 a [ i ] a[i] a[i]条边,这实在是难以承受的开销
注意到,所有的边都是连续的,我们可以利用线段树建图
算是一种积累的建图技巧了
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e6+100;
const int inf = 1e9;
vector<array<int,3>> G[maxn];
int a[maxn],b[maxn];
int fa[maxn],pa[maxn];
int n;
int d[maxn];
int dij(int s,int t)
{
priority_queue<array<int,2>> que;
for (int i=1;i<=t;++i)d[i] = inf;
que.push({0,s});d[s]=0;
while (!que.empty())
{
array<int,2> p = que.top();
que.pop();
int u = p[1];
if (-p[0]>d[u])continue;
for (array<int,3> ar:G[u])
{
int v = ar[0];
int cost = max(0,ar[2]);
if (d[v]>d[u]+cost)
{
fa[v] = u;
d[v] = d[u]+cost;
pa[v] = ar[1];
que.push({-d[v],v});
}
}
}if (d[t]==inf)return -1;
return d[t];
}
int tot;
struct stree
{
array<int,2> rot[maxn];
void build(int rt,int l,int r)
{
if (l==r)
{
rot[rt] = {l-b[l],l};
pa[rt] = l;
return;
}
rot[rt] = {++tot,-1};
int mid = l+r>>1;
build(rt<<1,l,mid);
build(rt<<1|1,mid+1,r);
G[rot[rt][0]].push_back({rot[rt<<1][0], rot[rt<<1][1], 0});
G[rot[rt][0]].push_back({rot[rt<<1|1][0], rot[rt<<1|1][1], 0});
}
void query(int rt,int l,int r,int ql,int qr,int s)
{
if (l>=ql&&r<=qr)
{
G[s].push_back({rot[rt][0],rot[rt][1],1});
return;
}int mid = l+r>>1;
if (mid>=ql)query(rt<<1,l,mid,ql,qr,s);
if (mid+1<=qr)query(rt<<1|1,mid+1,r,ql,qr,s);
}
}S1;
int main()
{
cin>>n;tot=n;
for (int i=1;i<=n;++i)cin>>a[i];
for (int i=1;i<=n;++i)cin>>b[i];
reverse(a+1,a+1+n);reverse(b+1,b+1+n);
S1.build(1,1,n);
int t = ++tot;
for (int i=1;i<=n;++i)
{
if (i+a[i]>n)G[i].push_back({t,-1,1});
else S1.query(1,1,n,i,i+a[i],i);
}
int ans = dij(1,t);
cout<<ans<<endl;
if (ans==-1)return 0;
int cur = t;
vector<int> res;
while (cur != 1)
{
// cout<<cur<<" ";
if (cur<=n&&cur>=1&&pa[cur]!=-1)res.push_back(pa[cur]);
if (pa[cur]!=-1)res.push_back(pa[cur]);
cur = fa[cur];
}reverse(res.begin(),res.end());
for (int i=0;i<res.size();i+=2)cout<<n+1-res[i]<<" ";cout<<"0"<<endl;
}
\newpage
E.分治
给你一个长度为 n n n的数组 a a a和一个长度为 m m m的数组 b b b
你要保证 a a a中的元素相对位置不变,把 b b b中元素插入到 a a a中,最后得到一个长度为 n + m n+m n+m的数组 c c c
最小化这个得到的数组的逆序对数。
很明显我们应当把 b b b数组从小到大插入进去,这样才能保证 b b b数组,自身的逆序对贡献为零
因此,先对 b b b数组排序
记 s o l v e ( l 1 , r 1 , l 2 , r 2 ) solve(l_1,r_1,l_2,r_2) solve(l1,r1,l2,r2)表示数组 b [ l 1 : r 1 ] b[l_1:r_1] b[l1:r1]与数组 a [ l 2 : r 2 ] a[l_2:r_2] a[l2:r2]的答案
假设,我们确定将 b i b_i bi插入 a j a_j aj与 a j + 1 a_{j+1} aj+1之间,那么我们实际上可以递归 s o l v e ( 1 , i − 1 , 1 , j ) , s o l v e ( i + 1 , n , j + 1 , m ) solve(1,i-1,1,j),solve(i+1,n,j+1,m) solve(1,i−1,1,j),solve(i+1,n,j+1,m)
采用分治法,关键是如何确定 b i b_i bi插入的位置
为了保证分治法的时间复杂度,我们你可以每次取 b m i d b_{mid} bmid
这里有一点贪心的思想,我们只用考虑 b m i d b_{mid} bmid在 l 2 , r 2 l_2,r_2 l2,r2中构成逆序对最少的那个位置即可
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6+10;
#define int long long
int msort(int a[],int l,int r)
{
if(l >= r) return 0;
int mid = l+r>>1;
int le = msort(a,l,mid),ri = msort(a,mid+1,r);
int cnt = 0;
int now = 0;
int i=l,j=mid+1;
int copy[r-l+2];
while(i<=mid && j <= r)
{
if(a[i] <= a[j]) copy[++now] = a[i++];
else{
cnt += mid-i+1;
copy[++now]=a[j++];
}
}
while(i <= mid){
copy[++now] = a[i++];
}
while(j <= r){
copy[++now] = a[j++];
}
now = 0;
for(int k = l; k <= r; k++) a[k] = copy[++now];
return le+cnt+ri;
}
void getpos(int a[],int b[],int pos[],int l,int r,int L,int R)
{
if(l>r) return;
int mid = l+r>>1;
int loc = L,now = 0,mn=0;
for(int i = L+1;i <= R; i++){
if(a[i-1] > b[mid]) now++;
if(a[i-1] < b[mid]) now--;
if(now < mn){
mn = now;
loc = i;
}
}
pos[mid] = loc;
getpos(a,b,pos,l,mid-1,L,loc);
getpos(a,b,pos,mid+1,r,loc,R);
}
void solve()
{
int n,m;
cin>>n>>m;
int a[n+1],b[m+1],pos[m+1];
for(int i = 1; i <= n; i++) cin>>a[i];
for(int i = 1; i <= m; i++) cin>>b[i];
sort(b+1,b+1+m);
getpos(a,b,pos,1,m,1,n+1);
int c[n+m+1],cnt=0,now=0;
for(int i = 1; i <= m; i++){
while(pos[i] - 1 > now) c[++cnt] = a[++now];
c[++cnt] = b[i];
}
while (now < n) c[++cnt] = a[++now];
cout << msort(c,1,n+m) << "\n";
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;cin>>t;
while(t--)
{
solve();
}
}
\newpage
F.贪心
有 n ( 1 ≤ 5 × 1 0 5 ) n(1\leq 5\times 10^5) n(1≤5×105) 位登山者和一座初始高度为 d ( 0 ≤ d ≤ 1 0 9 ) d(0\leq d\leq 10^9) d(0≤d≤109) 的山,每位登山者有两个属性 s i , a i s_i,a_i si,ai,其中 s i s_i si 代表登山者 i i i 只能攀登高度小于等于 s i s_i si 的山,当登山者 i i i 成功登山后,山的高度会变为 max ( d , a i ) \max(d,a_i) max(d,ai)。
找到一个最佳的登山顺序,保证有最多的人成功登山,输出最多人数。
首先,排除掉所有
s
i
<
d
s_i<d
si<d 的人。
以
max
(
s
i
,
a
i
)
\max(s_i,a_i)
max(si,ai) 进行升序排序,如果相等按照
s
i
s_i
si 排序。排序后,对每个登山者
i
i
i 进行判断即可。
考虑两个登山者
i
,
j
i,j
i,j,进行分类讨论:
1、 s i < a j s_i<a_j si<aj,如果把 j j j 放在前面, j j j 能成功登山, i i i 一定无法登山,应该先让 i i i 进行尝试。
2、 a i < s j a_i<s_j ai<sj, j j j 的能力比 i i i 强,放在后面一定不劣。
3、 s i < s j s_i<s_j si<sj,同情况2,一定不劣。
4、 a i < a j a_i<a_j ai<aj, j j j 成功登山后 i i i 一定无法登山,应该把 i i i 放在前面先进行尝试。
时间复杂度 O ( n log n ) O(n\log n) O(nlogn)。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct node{
int s,a;
}A[N];
bool cmp(node A,node B){
if(max(A.s,A.a)==max(B.s,B.a))
return A.s<B.s;
return max(A.s,A.a)<max(B.s,B.a);
}
int main(){
int n,d,ans=0;
cin>>n>>d;
for(int i=1;i<=n;i++)
scanf("%d %d",&A[i].s,&A[i].a);
sort(A+1,A+1+n,cmp);
for(int i=1;i<=n;i++)
if(d<=A[i].s)
d=max(d,A[i].a),ans++;
cout<<ans<<endl;
return 0;
}