反思
- 这次倒是把能拿的暴力分都拿到了,也思考到了最后,虽然T2正解最终也没有想出来吧
- T1其实思路已经很接近了,考后按着自己的想法改了一下就过了,当时最后几分钟(hack)掉了自己的算法,可能还是有点慌了吧
- T3当时其实就是按反素数的思路想的,但是不会约数和公式,甚至线性筛都忘了
- T4一眼线段树,但是维护什么,怎么维护?就并没有什么想法了
- 总体来说,还是见的题太少了,所以一看新题脑子里没什么可以类比的思路,还是硬实力缺乏吧,所以之后多加总结为好。
简述
T1 折纸
模拟题
它的确很简单,但我不敢说他简单了,因为无论多简单的题我都会写挂。。。
其实就是每次有两种情况,一种是左边部分向右折,一种是右边部分向左折
我们能想到需要确定折后每点的位置,但是n的范围是1e18,这样肯定会炸
但是m的范围是3000,这是一个n方的标志,所以,我们只需要考虑折叠点的位置即可
所以,我们应该在折叠后把这之后的折叠点位置直接修改,如果折的是当前折叠点左侧,而某个折叠点刚好在当前折叠点的左侧,那么就把它按折叠点对称过去
另外,如果我们总是折左右两边比较长的那边,显然折后的左/右端点就是这个折叠点
我们用(l,r)记录当前的左右端点,最后的答案即为(r-l)
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define M 3005
ll T,n,m,d[M];
ll l,r;
int main()
{
scanf("%lld",&T);
while(T--)
{
memset(d,0,sizeof d);
scanf("%lld%lld",&n,&m);
l=0;r=n;
for(int i=1;i<=m;i++)scanf("%lld",&d[i]);
for(int i=1;i<=m;i++)
{
if(d[i]-l<r-d[i])
{
for(int j=i+1;j<=m;j++)
{
if(d[j]<d[i])d[j]=d[i]*2-d[j];
}
l=d[i];
}
else
{
for(int j=i+1;j<=m;j++)
{
if(d[j]>d[i])d[j]=d[i]*2-d[j];
}
r=d[i];
}
//cout<<l<<' '<<r<<endl;
}
printf("%lld
",abs(r-l));
}
return 0;
}
T2 water
为什么总是不会搜索题呢
解法1:bfs
因为水是从里往外流的,我们不如从外往里走,根据水桶原理,水最终的绝对高度肯定等于他和外界联系中最低的位置,因此,我们先将边界上的点加入一个小根堆(这样就能保证每个点都是被他旁边最低的点遍历到的,可以保证遍历到它的这条路径上最大值最小),然后做宽搜
宽搜时,我们让每个位置水的绝对高度取块高和遍历他的位置水的绝对高度的max,最后用水的绝对高度减去块高,即可得到水的相对高度
- 注意:
priority_queue
默认为大根堆,如果需要小根堆,需要把运算符反着重载
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 305
const ll dx[4]={0,0,1,-1};
const ll dy[4]={1,-1,0,0};
ll n,m;
ll a[N][N],h[N][N];
ll vis[N][N];
struct node
{
ll x,y,h;
node(ll xx,ll yy,ll hh){x=xx,y=yy,h=hh;};
};
bool operator<(node a,node b)
{
return a.h>b.h;
}
priority_queue<node>q;
int main()
{
//freopen("owo.in","r",stdin);
//freopen("awa.out","w",stdout);
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%lld",&a[i][j]);
for(int i=1;i<=n;i++)
{
if(a[i][1]<0)h[i][1]=0;else h[i][1]=a[i][1];
if(a[i][m]<0)h[i][m]=0;else h[i][m]=a[i][m];
q.push(node(i,1,h[i][1]));
q.push(node(i,m,h[i][m]));
vis[i][1]=vis[i][m]=1;
}
for(int j=1;j<=m;j++)
{
if(a[1][j]<0)h[1][j]=0;else h[1][j]=a[1][j];
if(a[n][j]<0)h[n][j]=0;else h[n][j]=a[n][j];
q.push(node(1,j,h[1][j]));
q.push(node(n,j,h[n][j]));
vis[1][j]=vis[n][j]=1;
}
while(!q.empty())
{
node t=q.top();
q.pop();
ll x=t.x,y=t.y,hh=t.h;
for(int i=0;i<4;i++)
{
ll xx=x+dx[i],yy=y+dy[i];
if(xx<=0||xx>n||yy<=0||yy>m)continue;
if(vis[xx][yy])continue;
vis[xx][yy]=1;
h[xx][yy]=max(a[xx][yy],hh);
q.push(node(xx,yy,h[xx][yy]));
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
printf("%lld ",h[i][j]-a[i][j]);
}
puts("");
}
return 0;
}
解法2:最小生成树
需要理解的是:每一格的水高取决于这个位置到格外的所有路径中,所有路径的最高点中,的最小值
我们将二维坐标抽象成一维,将所有相邻位置连边,边权为两个位置较高的高度,将所有边界位置与0连边,边权为(max(a[i][j],0))
由我们之前的分析,我们可以得到,以所有路径的最高点中的最小值为边权的边,一定属于连边后整个图的最小生成树,为什么呢,因为最小生成树中这个位置到根节点(0)的路径只有一条,肯定是取那条最小的
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 305
const ll dx[4]={0,0,1,-1};
const ll dy[4]={1,-1,0,0};
ll n,m;
ll a[N][N],h[N][N];
ll vis[N][N];
struct node
{
ll x,y,h;
node(ll xx,ll yy,ll hh){x=xx,y=yy,h=hh;};
};
bool operator<(node a,node b)
{
return a.h>b.h;
}
priority_queue<node>q;
int main()
{
//freopen("owo.in","r",stdin);
//freopen("awa.out","w",stdout);
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%lld",&a[i][j]);
for(int i=1;i<=n;i++)
{
if(a[i][1]<0)h[i][1]=0;else h[i][1]=a[i][1];
if(a[i][m]<0)h[i][m]=0;else h[i][m]=a[i][m];
q.push(node(i,1,h[i][1]));
q.push(node(i,m,h[i][m]));
vis[i][1]=vis[i][m]=1;
}
for(int j=1;j<=m;j++)
{
if(a[1][j]<0)h[1][j]=0;else h[1][j]=a[1][j];
if(a[n][j]<0)h[n][j]=0;else h[n][j]=a[n][j];
q.push(node(1,j,h[1][j]));
q.push(node(n,j,h[n][j]));
vis[1][j]=vis[n][j]=1;
}
while(!q.empty())
{
node t=q.top();
q.pop();
ll x=t.x,y=t.y,hh=t.h;
for(int i=0;i<4;i++)
{
ll xx=x+dx[i],yy=y+dy[i];
if(xx<=0||xx>n||yy<=0||yy>m)continue;
if(vis[xx][yy])continue;
vis[xx][yy]=1;
h[xx][yy]=max(a[xx][yy],hh);
q.push(node(xx,yy,h[xx][yy]));
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
printf("%lld ",h[i][j]-a[i][j]);
}
puts("");
}
return 0;
}
T3 找伙伴
数学题,原题是luogu上聪明的燕姿
其实有些地方和前天的反质数有些相像
核心是两个柿子:
- 对于任意一个大于1的整数(A),(A)可以表示成:
- 对于任意一个大于1的整数(A),(A)的所有正约数和(S)可以表示成:
其中,
可以理解成一个从质因数里选一些乘起来的过程
那么,问题就转化为:给定(S),求所有满足条件的(A)
在之前的柿子中我们看出,(S)是由一个个(sum_{p_i,a_i})乘起来的
那么,我们可以枚举每一个(sum_{p_i,a_i}),然后类比反素数的方法,做一个dfs
这里有一个小技巧:数据范围是(2e9)的情况下,如果用线性筛把(2e9)的质数全都筛出来,会(T)掉,所以我们可以只筛出(1e5)的质数,如果问到了没有筛到的,就用现在筛到的所有质数来判断
在搜索时:传递3个参数:一是现在剩余的约数和(rem),二是现在走到了第几个质数(no),三是当前的答案(now)
如果剩余约数和为1,说明此时原(w)已经被分解完毕了,这个时候,(now)便是满足条件的一个解
如果剩余约数和为(p_k+1),这个时候只要再除一个(sum_{p_k,1})即(p_k+1)就能得到答案了,所以这时(now*(p_k+1))是一个解
为什么一定要判定这种情况呢?
- 如果不判定这种情况,后来的转移中,枚举质数要一直走到(rem),而判断这种情况后,我们只用走到(sqrt{rem}),因为在(sqrt{rem}leq p_k< rem-1)之间的质数此时(sum_{p_k,1}=p_k+1)定小于(rem),而(sum_{p_k,2}=p_k^2+p_k+1)又定大于(rem),所以(sqrt{rem}leq p_k< rem-1)这一段肯定没有解
- 同时,如果枚举质数要走到(rem),若(rem)比较大,交给之后来筛的话,质数只筛到了(1e5),就无法往下进行了
接下来,我们枚举质数,找到下一个可以除的(sum_{p_?,a_?}),因为之前的判断,这个时候我们只需要枚举到(sqrt{rem})就行,这里可以预处理出(sum)数组,但是质数很多,弄不好就爆炸了,所以可以一边找一边计算
如果找到了,就把(rem/=sum_{p_?,a_?}),同时新(no)设为当前走到的质数编号+1,(now*=p_?^?),这个次方也是一边找一边算
最后把得到的答案排序输出即可
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 100000
ll n,w;
bool isp[N];
ll prime[N],cnt=0;
void getprime()
{
memset(isp,1,sizeof isp);
isp[1]=0;
for(int i=2;i<=N;i++)
{
if(isp[i])prime[++cnt]=i;
for(int j=1;j<=cnt&&prime[j]*i<=N;j++)
{
isp[prime[j]*i]=0;
if(i%prime[j]==0)break;
}
}
}
bool isprime(ll x)
{
if(x<=N)return isp[x];
for(int i=1;prime[i]*prime[i]<=x;i++)if(x%prime[i]==0)return 0;
return 1;
}
ll ans[N];
ll tot=0;
void dfs(ll rem,ll no,ll now)
{
if(rem==1)
{
ans[++tot]=now;
return;
}
if(isprime(rem-1)&&rem>prime[no])
{
ans[++tot]=now*(rem-1);
}
for(int i=no;prime[i]*prime[i]<=rem;i++)
{
ll po=prime[i];
ll psum=prime[i]+1;
for(;psum<=rem;po*=prime[i],psum+=po)
{
if(rem%psum==0)dfs(rem/psum,i+1,now*po);
}
}
}
int main()
{
//freopen("owo.in","r",stdin);
getprime();
//for(int i=1;prime[i]<100;i++)printf("%lld ",prime[i]);
//cout<<endl;
while(scanf("%lld",&w)!=EOF)
{
tot=0;
memset(ans,0,sizeof ans);
dfs(w,1,1);
printf("%lld
",tot);
sort(ans+1,ans+tot+1);
if(tot)for(int i=1;i<=tot;i++)printf("%lld ",ans[i]);
if(tot)puts("");
}
}
T4 string
利用线段树
题目需要支持将一个字符串给定的(l~r)区间升序/降序排列
这里是一个小技巧:排列的时候其实就是把段里的所有字母统计个数,再重新插入进去,这是可以用线段树维护的,只需要推敲一下(up),(down)什么的
因此,我们需要支持这些操作:将([l~r])改为(c),求出([l~r])中每个字母的个数
我们用每个节点值(c)表示这个结点管的区间中所有字母都是(c),如果有不同,就为0
建树:正常流程,但要注意应当(s[l]-'a'+1),防止出现这里有其他意义的0
(up):如果左右相等,这个结点也跟他们相等,否则为0
(down):左右直接赋为这个结点值
修改:找到被所求区间包含的区间或者这个区间已经全为(c)了,就修改结点值
询问:找到一个区间所有全为(c)时,(cnt[c]++)
题中要求的升/降序排列,我们就每次记录所给区间中(cnt),然后按顺序修改
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 100005
#define lson rt<<1,l,m
#define rson rt<<1|1,m+1,r
#define ls rt<<1
#define rs rt<<1|1
char s[N];
ll cnt[30];
ll tr[N<<2];
void up(ll rt)
{
if(tr[ls]==tr[rs])tr[rt]=tr[ls];
else tr[rt]=0;
}
void down(ll rt)
{
if(!tr[rt])return;
tr[ls]=tr[rt];
tr[rs]=tr[rt];
}
void build(ll rt,ll l,ll r)
{
if(l==r)
{
tr[rt]=s[l]-'a'+1;
return;
}
ll m=(l+r)>>1;
build(lson);
build(rson);
up(rt);
}
void change(ll rt,ll l,ll r,ll nl,ll nr,ll k)
{
if(nl<=l&&r<=nr||tr[rt]==k)
{
tr[rt]=k;
return;
}
if(tr[rt])
{
tr[ls]=tr[rs]=tr[rt];
}
ll m=(l+r)>>1;
if(m>=nl)change(lson,nl,nr,k);
if(m<nr)change(rson,nl,nr,k);
up(rt);
}
void query(ll rt,ll l,ll r,ll nl,ll nr)
{
if(nl<=l&&r<=nr&&tr[rt])
{
cnt[tr[rt]]+=r-l+1;
return;
}
down(rt);
ll m=(l+r)>>1;
if(m>=nl)query(lson,nl,nr);
if(m<nr)query(rson,nl,nr);
}
void print(ll rt,ll l,ll r)
{
if(tr[rt])
{
for(int i=l;i<=r;i++)printf("%c",'a'+(int)tr[rt]-1);
return;
}
down(rt);
ll m=(l+r)>>1;
print(lson);
print(rson);
}
ll n,m,l,r,x;
int main()
{
scanf("%lld%lld",&n,&m);
scanf("%s",s+1);
build(1,1,n);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&l,&r,&x);
memset(cnt,0,sizeof cnt);
query(1,1,n,l,r);
if(x==1)
{
ll tmp=l;
for(int j=1;j<=26;j++)
{
change(1,1,n,tmp,tmp+cnt[j]-1,j);
tmp+=cnt[j];
}
}
else if(x==0)
{
ll tmp=l;
for(int j=26;j>=1;j--)
{
change(1,1,n,tmp,tmp+cnt[j]-1,j);
tmp+=cnt[j];
}
}
}
print(1,1,n);
return 0;
}