导语
涉及的知识点
字符串、字典树、思维、数学、前缀和、逆序对
题目
C
题目大意:给出四个数a,b,c,n,构造三个字符串,构造三个小写字符串,使得 ∣ s 1 ∣ = ∣ s 2 ∣ = ∣ s 3 ∣ = n , L C S ( s 1 , s 2 ) = a , L C S ( s 2 , s 3 ) = b , L C S ( s 1 , s 3 ) = c |s_1|=|s_2|=|s_3|=n,LCS(s_1,s_2)=a,LCS(s_2,s_3)=b,LCS(s_1,s_3)=c ∣s1∣=∣s2∣=∣s3∣=n,LCS(s1,s2)=a,LCS(s2,s3)=b,LCS(s1,s3)=c,按照输入顺序输出字符串,不存在就输出NO
思路:折磨,折磨,还是折磨,写的时候字符串都构造出来了,结果在按照输入顺序输出上拌了脚。用贪心的思路,求得a,b,c的最小值,假设为a,那么s1,s2,s3这三个字符串设置都使用统一的a个相同字符填充,最后根据b-a,c-a去构造另外两对字符串即可,详见代码
代码
#include <bits/stdc++.h>
using namespace std;
int d[3],n,pos,m=1212,len1,len2,len3,mm;
bool vis[3];
char s[4][1212],ch='a';
int main() {
ios::sync_with_stdio(0),cin.tie(0);
cin >>d[0]>>d[1]>>d[2]>>n;
for(int i=0; i<3; i++)
if(m>=d[i]) {
m=d[i];
pos=i;
}
mm=m;
for(int i=0; i<m; i++) {
s[1][len1++]=ch;
s[2][len2++]=ch;
s[3][len3++]=ch;
}
ch++;
m=1212;
vis[pos]=1;
for(int i=0; i<3; i++)
if(m>=d[i]&&!vis[i]) {
m=d[i];
pos=i;
}
m-=mm;
if(pos==0) {
for(int i=0; i<m; i++) {
s[1][len1++]=ch;
s[2][len2++]=ch;
}
} else if(pos==1) {
for(int i=0; i<m; i++) {
s[2][len2++]=ch;
s[3][len3++]=ch;
}
} else {
for(int i=0; i<m; i++) {
s[1][len1++]=ch;
s[3][len3++]=ch;
}
}
ch++;
m=1212;
vis[pos]=1;
for(int i=0; i<3; i++)
if(m>=d[i]&&!vis[i]) {
m=d[i];
pos=i;
}
m-=mm;
if(pos==0) {
for(int i=0; i<m; i++) {
s[1][len1++]=ch;
s[2][len2++]=ch;
}
} else if(pos==1) {
for(int i=0; i<m; i++) {
s[2][len2++]=ch;
s[3][len3++]=ch;
}
} else {
for(int i=0; i<m; i++) {
s[1][len1++]=ch;
s[3][len3++]=ch;
}
}
ch++;
if(len1>n||len2>n||len3>n)
cout <<"NO"<<endl;
else {
for(int i=len1; i<n; i++)
s[1][i]=ch;
ch++;
for(int i=len2; i<n; i++)
s[2][i]=ch;
ch++;
for(int i=len3; i<n; i++)
s[3][i]=ch;
for(int i=1; i<=3; i++)
cout <<s[i]<<endl;
}
return 0;
}
E
题目大意:给出具有n个节点的树与每个节点的取值范围 [ l i , r i ] [l_i,r_i] [li,ri],给出每个边权的值,边权值为端点间异或,询问有多少种节点赋值方案能满足给定条件
思路:首先节点的赋值方案只与根节点有关,假设根节点权值为 w 0 = a w_0=a w0=a,与根节点相邻的节点权值便可以确定了 w i = e i ⊕ a w_i=e_{i}\oplus a wi=ei⊕a,同理,其他点也可以确定下来,并且各点的权值表达式为 w t = e i ⊕ e i + 1 ⊕ ⋯ ⊕ a w_t=e_{i}\oplus e_{i+1}\oplus \dots \oplus a wt=ei⊕ei+1⊕⋯⊕a,可见都有a,因此对应给定的约束条件就变为了 l i ≤ e i ⊕ a ≤ r i l_i\le e_i\oplus a\le r_i li≤ei⊕a≤ri,于是问题转换为求满足各个点的约束条件的a有多少个,a的范围为 [ l i ⊕ e i , r ⊕ e i ] [l_i \oplus e_i,r\oplus e_i] [li⊕ei,r⊕ei],但是异或之后,所得的不一定为连续的范围,需要把它分成多个连续的区间
获得多个取值区间后,需要求得这些区间的交集,交集中的元素个数即为方案个数,基本的思想就是记录每个点被覆盖的次数(差分),找到覆盖了n次的点有几个即可,但是由于区间不连续,所以不能直接暴力
对于整个不连续的区间,需要将他们切割成连续区间,首先可以将 [ l i , r i ] [l_i,r_i] [li,ri]用二进制分割成多个区间长度为2的整数次幂的区间,分割之后的区间有一个性质,假设分割后的区间为 [ x , x + 2 t − 1 ] [x,x+2^t-1] [x,x+2t−1],该区间内每个数的前k位都是相同的(如图,用二进制的原理也很容易想到),对该区间异或操作分为前k位和后n-k位来讨论,对前k位异或同一值显然不变,对后n-k位异或后得到的区间仍然为自身,以取模的思想来理解,这样 [ l i , r i ] [l_i,r_i] [li,ri]被分成了多个异或之后仍然连续的区间。
根据数据范围,可以用230的总长度去构造每个
[
l
i
,
r
i
]
[l_i,r_i]
[li,ri]的切割区间,对于这些区间进行+1(差分),累计被覆盖次数为n的点即可,由于数据范围过大,只能把差分的首和尾用结构体存储并排序,计算时遍历结构体更新答案
关于operation函数
这个函数是实现切割的关键部分,首先明确一点,在函数中对给定区间异或实质上为对区间各个数的前k位异或,因为对剩下的位异或之后组成的仍然为区间。len用来获取区间长度,a1用来获取区间异或之后的左端点,len-1获取的是后n(n只是l的总位数)-k位全1时的值,~(len-1)将这些位边为0,val&( ~(len-1) )将异或值的n-k为置0,因为这些位异或后区间仍然连续,不会修改区间的大小与位置,l^(val&( ~(len-1) ))最后获得执行异或操作之后的l (val的前k位与l异或),由于区间长度不变,l+len便可以得到r
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,l[maxn],r[maxn],head[maxn],tot,W[maxn],ans,sum;
struct node {
int next,to,w;
} e[2*maxn];//链式前向星
struct Seg {
int val,lazy;
bool operator<(const Seg&t)const {
if(val!=t.val)
return val<t.val;
return lazy<t.lazy;
}
};
vector<Seg>V;
void Add(int from,int to,int w) {
e[++tot].to=to;
e[tot].next=head[from];
e[tot].w=w;
head[from]=tot;
}
void DFS(int u,int fa,int val) {
W[u]=val;
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to,p=e[i].w;
if(v!=fa)
DFS(v,u,val^p);
}
}
void operation(int l,int r,int val) {
Seg a1,a2;
int len=r-l+1;//区间长度
a1.val=(l^(val&(~(len-1))));//确认左端点
a2.val=(l^(val&(~(len-1))))+len;//获取右端点
a1.lazy=1,a2.lazy=-1;
V.push_back(a1);
V.push_back(a2);
}
void BinarySearch(int L,int R,int l,int r,int val) {
//把区间二分到完美线段树的具体位置上,即一个个完整的二进制区间
if(L<=l&&R>=r) {//找到对应区间在哪
operation(l,r,val);//被覆盖,差分操作
return ;
}
int mid=(l+r)>>1;
if(L<=mid)
BinarySearch(L,R,l,mid,val);
if(R>mid)
BinarySearch(L,R,mid+1,r,val);
}
int main() {
ios ::sync_with_stdio(0),cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++)//存范围
cin >>l[i]>>r[i];
for(int i=1; i<=n-1; i++) {//录入边
int u,v,w;
cin >>u>>v>>w;
Add(u,v,w);
Add(v,u,w);
}
DFS(1,0,0);//构造初始树,假设根节点值为0
for(int i=1; i<=n; i++)
BinarySearch(l[i],r[i],0,(1<<30)-1,W[i]);//对0~(1<<30)-1模拟线段树操作
sort(V.begin(),V.end());//对差分用的结构体排序
int len=V.size();
for(int i=0; i<len-1; i++) {
sum+=V[i].lazy;
if(sum==n)//该点到下一点前面的区间覆盖了n次,说明满足了n个异或不等式
ans+=V[i+1].val-V[i].val;
}
cout <<ans;
return 0;
}
本题还有字典树写法,链接在文章末,之后有时间会再加上
F
题目大意:一个无向图,A和B轮流操作,要么拿走一条边,要么拿走一个无环的连通分量,谁最后拿不了判负,假设两者都执行最优策略,判断谁赢
思路:开始把题目想复杂了,对于第一种操作, 会使得边数 -1,对于第二种操作, 会使得点数 -k, 边数 -(k-1),最后答案只和(n+m)奇偶性有关
代码
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n,m;
cin >>n>>m;
int a,b;
for(int i=0;i<m;i++)
cin >>a>>b;
if((n+m)%2==1)
cout <<"Alice"<<endl;
else
cout <<"Bob"<<endl;
return 0;
}
I
题目大意:给出一个1~n的排列,定义序列权重为逆序对个数,现在能对每个位置的值+1或者不变,求出经过最优策略后能获得的权重最小值
思路:队友写的,逆序对的部分本来可以用树状数组,但是我当时在做字符串…队友找了合并排序的模板花费了些时间解出来了,以后有数据结构的简化部分不能放过
用贪心的思想去构造,对于x,如果x-1的位置在x之后,增大x-1对应位置上的值,即构造出一对x,x,标记x-1已经使用,继续遍历完,对遍历完的数据统计逆序对即可
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,pos[maxn],vis[maxn],a[maxn],tree[4*maxn];
long long ans;//注意long long
void update(int x) {
for(; x<=n; x+=-x&x)
tree[x]++;
}
int sum(int x) {
int acc=0;
for(; x; x-=x&-x)
acc+=tree[x];
return acc;
}
int main() {
ios::sync_with_stdio(0),cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++) {
cin >>a[i];
pos[a[i]]=i;
}
for(int i=2; i<=n; i++)
if(pos[i-1]>pos[i]&&!vis[i-1]) {
a[pos[i-1]]++;
vis[i]=1;
}
for(int i=1; i<=n; i++) {//统计逆序对
ans+=i-1-sum(a[i]);
update(a[i]);
}
cout <<ans;
return 0;
}
逆十字的代码
先树状数组,再减去减少的逆序对
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,a[maxn],t[4*maxn],p[maxn];
long long ans;//开long long 统计
void update(int x) {
for(; x<=n; x+=x&-x)
t[x]++;
}
int query(int x) {
int res=0;
for(; x; x-=x&-x)
res+=t[x];
return res;
}
int main() {
ios ::sync_with_stdio(0),cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++) {
cin >>a[i];
p[a[i]]=i;
}
for(int i=1; i<=n; i++) {//获得原始的逆序对个数
ans+=i-1-query(a[i]);//i-1是因为以已经使用过的数为基准,判断在a[i]之前的个数
update(a[i]);
}
bool flag=0;
for(int i=2; i<=n; i++) {
if(p[i]>p[i-1])//满足条件略过
flag=0;//更新标记
else if(flag)//代表这个位置已经使用过
flag=0;
else
ans--,flag=1;//减少一个相邻的对
}
cout <<ans;
return 0;
}
J
题目大意:n×m的矩阵W,给出两个序列 a 1 … n , b 1 … m , W i , j = a i + b j a_{1\dots n},b_{1\dots m},W_{i,j}=a_i+b_j a1…n,b1…m,Wi,j=ai+bj,现在求出一个拥有最大平均值的子矩阵,给出子矩阵的高和宽至少为多少,求出最大平均值的子矩阵的平均值
思路:分成横纵两个部分来处理,分别获取两部分各自符合条件的最大平均值,累加。使用二分来探索平均值,即每次以上界+下界的一半判断能否作为平均值,其他详见代码
关于judge函数
在judge函数中,构造了b数组存储原值与平均值的差值,并构造b数组的前缀和,对于一个连续的b的区间和sum,如果sum<0,代表给定平均值无法作为该区间的平均值,如果sum>=0,代表该区间平均值≥给定平均值,在judge函数的后半段中,为了确保区间长度,sum[i]中i必须从给定长度f开始取,所求得的sum值,实质上为sum[i]-sum[x],在代码中,所得的minn一定≤0,因为sum[0]为0,因此,minn包含的区间只会使得平均值减小,需要减去,逐步扩大i的取值,每次求得减去之后的最值即可
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,m,x,y,a[maxn];//a存数据
double res,b[maxn],sum[maxn];//b为a数组减去假设平均值
double judge(int n,double mid,int f) {//判断是否可行
for(int i=1; i<=n; i++) {
b[i]=a[i]-mid;//获得每一项与平均值的差值
sum[i]=sum[i-1]+b[i];//累和差值,如果最终差值为负数,代表此平均值不行
}
double minn=1e10,maxx=-1e10;
for(int i=f; i<=n; i++) {//保证高度
minn=min(minn,sum[i-f]);//获得值最小的从初始开始的区间
maxx=max(maxx,sum[i]-minn);//sum[i]累和到当前位置的累和,判断去掉前置区间的最值
}
//类似尺取法
return maxx;
}
double largestAve(int n,int f) {
double mid,l=0,r=1e5,eps=1e-8;
while(r-l>eps) {//二分平均值,尝试能否以该值为平均值
mid=(r+l)/2;
if(judge(n,mid,f)>0)//如果获得平均值可用
l=mid;
else
r=mid;
}
return r;
}
int main() {
cin >>n>>m>>x>>y;
for(int i=1; i<=n; i++)
cin >>a[i];
res+=largestAve(n,x);//获取纵列最大平均值
for(int i=1; i<=m; i++)
cin >>a[i];
res+=largestAve(m,y);//获取横排最大平均值
printf("%.10lf",res);
return 0;
}