CSYZDay2模拟题解

T1.rotate

【问题描述】

ZYLN张牌编号分别为1, 2……N。他把这N张牌打乱排成一排,然后他要做一次旋转使得旋转后固定点尽可能多。如果第i个位置的牌的编号为i,我们就称之为固定点。旋转可以被认为是将其中的一个子段旋转180度,这意味着子段的第一张牌和最后一张牌交换位置,以及第二张牌和倒数第二张牌交换位置,等等。写一个程序,找到旋转子段(子段长度可以为1)。

 

【输入】

第一行包含一个整数 N (1 ≤ N ≤100 000)

第二行有N个数,第i个数表示旋转之前第i个位置的牌的编号。

输出】

找到固定点最多的旋转所选的子段,输出旋转之后固定点的个数。

 

【输入输出样例】

样例1

样例2

rotate.in

rotate.out

rotate.in

rotate.out

4

3 2 1 4

4

2

1 2

2

样例解释:

在样例1中,只需要旋转的子段[3,2,1],将排列变成1 2 3 4,旋转后所有的牌都为固定点。答案为4

在样例2中,所有的牌已经在固定点,旋转子段[1]或者子段[2],答案为2

 

【数据范围】

    30%的数据满足:N ≤ 500

  60%的数据满足:N ≤ 5000

100%的数据满足:1 ≤ N ≤ 100 000

注意本题题目描述有误,最后的数据应该是5*10^5。

这个题怎么做呢……考试的时候只想出来n^3暴力结果还特别智障的写错了参数导致爆零。

30分就是直接枚举所有区间之后直接判断。60分的发现也很好想,就是每次枚举旋转区间的中心点,之后再枚举旋转的长度(注意这里是从短到长,就像莫队一样往两边扫统计答案,这样才是n^2的复杂度

至于100分的做法,我们首先假设现在最优的反转区间是[l,r],那么如果有a[l] != r &&  a[r] != l,那么也就相当于反转这一次区间对于这个区间两个端点是没有任何作用的,也就是说这两个端点对答案没有任何贡献,所以直接删去即可,那么[l+1,r-1]也是一个最优解。依次类推,直到出现一个位置使得a[l] == r || a[r] == l.

 

那么也就是说,一个区间如果想成为最优解,那么必须满足左右端点之中最少有一个有反转过后成为不动点。也就是答案必定在所有的[min(a[i],i),max[(a[i],i)]之中产生,其中i取遍1~n。那么我们就变成了一个查询问题,我们只需要查询n次即可获得结果,问题在于怎么在N次查询之中快速获取反转区间后的不动点个数。我们知道,对于一个反转区间,如果在反转过后其内部有点成为不动点,那么在反转之前,这个点必然满足a[j] + j == a[i] + i.,并且这个点对也可能有成为最优解的反转区间。所以我们把所有的旋转区间按照中心旋转点分类(每个用一个vector存起来),之后每次按照反转区间长度排序(我们要从小的开始取,之后再取大的),然后每次判断的时候我们发现这个点再vector中的位置,就是它内部所包含的反转过后能成为不动点的点的数目(因为比他小的都包含在里面,随着它反转必然成为不动点),然后还要加上一(这个点本身的贡献)。这样的话,剩下的不反转的区间直接使用前缀和维护不动点个数即可。总复杂度O(nlogn)。

 

看一下代码。

 

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
#include<vector>
#include<set>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 500005;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-') op = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        ans *= 10;
        ans += ch - '0';
        ch = getchar();
    }
    return ans * op; 
}

vector <int> g[M<<1];
int n,a[M],sum[M],cur,maxn = -1;

bool cmp(int x,int y)//按反转的长度排序
{
    return abs((x<<1)-cur) < abs((y<<1)-cur);
}

int main()
{
//    freopen("a.in","r",stdin);
//    freopen("rotate1.out","w",stdout);
    n = read();
    rep(i,1,n) 
    {
        a[i] = read();
        g[a[i]+i].push_back(i);//压入vector
        if(a[i] == i) sum[i] = sum[i-1] + 1;
        else sum[i] = sum[i-1];//前缀和
    }
    rep(i,2,n<<1)//枚举反转中心
    if(!g[i].empty())
    {
        cur = i;
        sort(g[i].begin(),g[i].end(),cmp);//排序
        int k = g[i].size();
        rep(j,0,k-1)
        {
            int l = g[i][j];
            int r = i - g[i][j];
            if(l > r) swap(l,r);//找到区间左右端点
            maxn = max(maxn,sum[l-1] + sum[n] - sum[r] + j + 1);//更新答案(原理如上文所述)
        }
    }
    printf("%d\n",maxn);
    return 0;
}

 

T2.cell

【问题描述】

CYJ想找到他的小伙伴FPJ.CYJFPJ现在位于一个房间里,这个房间的布置可以看成一个NM列的矩阵,矩阵内的每一个元素会是下列情况中的一种:

  1. 障碍区域这里有一堵墙(‘#’表示).
  2. 这是CYJ最开始在的区域(‘C’表示).
  3. 这是FPJ在的区域(‘F’表示).
  4. 空区域(‘.’表示).

CYJ携带了一个所谓传送枪的东西,这是一把可以创造传送门的枪械,在每一次行动中,他可以选择下列操作中的一项:

  1. 1.移向一个相邻的格子中(,,,,不能移到墙在的格子里).这个操作要消耗一个单位的时间.
  2. 2.转向一个墙(不需要相邻,只需面向即可),向其发射传送门,传送门会留在墙内面向你的地方(至多只能同时存在两扇传送门),若墙上已经有两扇传送门,而你发射了第三扇,那么最初发射的那一扇会消失。同时,你无法在一个位置制造两扇传送门(这个操作不会耗费时间)。
  3. 3.如果他与一块墙壁相邻且面前有一扇传送门,那么他可以移动到另一扇传送门前方的格子。这个操作会耗费一个单位的时间.

CYJ想要知道自己最少需要多少时间才能够从起点(‘C’)到达终点(‘F’).

请注意:我们保证地图边缘会是一圈墙壁且一定存在‘C’,‘F’.

【输入】

第一行输入两个正整数 N M ,(4   N,M ≤ 500).表示地图大小。

接下来的N行每行一个长度为M的字符串.表示地形。

输出】

    你需要输出最少的到达终点的时间,如果不能到达请输出”no”

【输入输出样例】

样例1

样例2

样例3

cell.in

4 4

####

#.F#

#C.#

####

cell.out

2

cell.in

6 8

########

#.##..F#

#C.##..#

#..#...#

#.....##

########

 

cell.out

4

cell.in

#####

#C#.#

###F#

#####

 

cell.out

no

样例2解释:

C(3,2)开始,我们首先向左发射传送门,再向下发射传送门,向左进入传送门,到达(5,2),向右发射传送门,向下进入传送门,到达(5,6),向上发射传送门,向右进入传送门,到达(2,6),向右移动,到达F.

 

【数据范围】

50%的数据满足:4<= N,M <=15

    100%的数据满足:4<= N,M <=500

这道题也不会……考试的时候瞎写一通骗了15pts。

那这题该怎么做呢……我们考虑之后发现,一个人在一个格子最多只能有八种行为:四种是向上下左右走一格,还有四种是向四个方向的墙上发射传送门,之后跑到最近的墙旁边传送过去。所以说,我们可以首先暴力的预处理出来对于每个点最近的墙在哪。

这个使用bfs,一开始把所有的墙全部存在里面进行向四个方向的宽搜。(把墙全部存起来是非常优秀的操作,可以有效防止超时,就好比你同时把两个点压入set(pq)跑dij,复杂度还是nlogn,比每一个点跑一次快很多)之后在暴力预处理出来对于每一个格子其四个方向最近的墙的位置(这个有点像悬线法,就是如果你左边有墙,那么就把最近的位置设为左边点,否则直接把再左边的答案赋过来)之后再建图,对于每一个点把所有的情况都建上,最后直接跑dij。

复杂度O(nmlog(nm)),可以过。然后注意每个点最多连八条边,这样的话边数开到4e6才好。

这种题以前还真没见过……应该练习了。

看一下代码。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<queue>
#include<set>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')
#define pr pair<int,int> 
#define mp make_pair
#define fi first
#define sc second

using namespace std;
typedef long long ll;
const int M = 505;
const int N = 500005;
const int INF = 100000009;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-') op = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        ans *= 10;
        ans += ch - '0';
        ch = getchar();
    }
    return ans * op;
}

struct node
{
    int next,to,v;
}e[N<<3];

int n,m,lw[M][M],rw[M][M],dw[M][M],uw[M][M],S,T,bs[M][M],ecnt,head[N],dis[N];
int dx[5] = {0,1,0,-1},dy[5] = {1,0,-1,0};
char s[M][M];
queue <pr> q;
set <pr> pq;
set <pr> :: iterator it;
int id(int x,int y)
{
    return (x-1) * m + y;
}

void ins(int x,int y,int d)
{
    if(s[x][y] == '.' && !bs[x][y]) bs[x][y] = d,q.push(mp(x,y));
}

void add(int x,int y,int z)
{
    e[++ecnt].to =y;
    e[ecnt].v = z;
    e[ecnt].next = head[x];
    head[x] = ecnt;
}

void bfs()
{
    while(!q.empty())
    {
        int kx = q.front().fi,ky = q.front().sc;
        q.pop();
        rep(i,0,3) ins(kx+dx[i],ky+dy[i],bs[kx][ky]+1);        
    }
}

void init()
{
    rep(i,1,n)
    rep(j,1,m)
    {
        if(s[i][j] != '.') continue;
        if(s[i][j-1] == '#') lw[i][j] = id(i,j);
        else lw[i][j] = lw[i][j-1];
        if(s[i-1][j] == '#') uw[i][j] = id(i,j);
        else uw[i][j] = uw[i-1][j];
    }
    per(i,n,1)
    per(j,m,1)
    {
        if(s[i][j] != '.') continue;
        if(s[i][j+1] == '#') rw[i][j] = id(i,j);
        else rw[i][j] = rw[i][j+1];
        if(s[i+1][j] == '#') dw[i][j] = id(i,j);
        else dw[i][j] = dw[i+1][j];
    }
}

void build()
{
    rep(i,1,n)
    rep(j,1,m)
    {
        if(s[i][j] != '.') continue;
        if(s[i][j+1] == '.') add(id(i,j),id(i,j+1),1),add(id(i,j+1),id(i,j),1);
        if(s[i+1][j] == '.') add(id(i,j),id(i+1,j),1),add(id(i+1,j),id(i,j),1);
        if(lw[i][j] != id(i,j)) add(id(i,j),lw[i][j],bs[i][j]);
        if(rw[i][j] != id(i,j)) add(id(i,j),rw[i][j],bs[i][j]);
        if(dw[i][j] != id(i,j)) add(id(i,j),dw[i][j],bs[i][j]);
        if(uw[i][j] != id(i,j)) add(id(i,j),uw[i][j],bs[i][j]);
    }
}

void dij()
{
    rep(i,1,n*m) dis[i] = INF;
    dis[S] = 0;pq.insert(mp(dis[S],S));
    while(!pq.empty())
    {
        pr now = *(pq.begin());
        pq.erase(pq.begin());
        for(int i = head[now.sc];i;i = e[i].next)
        {
            if(dis[e[i].to] > dis[now.sc] + e[i].v)
            {
                it = pq.find(mp(dis[e[i].to],e[i].to));
                if(it != pq.end()) pq.erase(it);
                dis[e[i].to] = dis[now.sc] + e[i].v;
                pq.insert(mp(dis[e[i].to],e[i].to));
            }
        }
    }
}

int main()
{
    n = read(),m = read();
    rep(i,1,n) 
    {
        scanf("%s",s[i]+1);
        rep(j,1,m) 
        {
            if(s[i][j] == 'C') s[i][j] = '.',S = id(i,j);
            if(s[i][j] == 'F') s[i][j] = '.',T = id(i,j);
            if(s[i][j] == '#') q.push(mp(i,j));
        }
    }
    bfs(),init(),build(),dij();
    if(dis[T] == INF) printf("no\n");
    else printf("%d\n",dis[T]);
    return 0;
}

 

T3.column

【问题描述】

WTH获得了一个柱状图,这个柱状图一共有N个柱子,最开始第i根柱子的高度为xi,他现在要将这个柱状图排成一个屋顶的形状,屋顶的定义如下:

  1. 屋顶存在一个最高的柱子,假设为i,最终高度为hi.它是所有柱子之中最高的.

  2. j根柱子的高度为hj=hi-|i-j|,但这个高度必须大于0,否则就是不合法的.

WTH可以对一个柱子做的操作只有将其高度加一或减一, WTH正忙着享受自己的人赢生活于是他将把这个柱状图变成屋顶的任务交给了你.你需要求出最少进行多少次操作才能够把这个柱状图变成一个屋顶形状.

【输入】

第一行包含一个正整数N(1 N 100 000).

第二行包含N个用空格隔开的正整数,表示xi,含义如题面。


输出】

输出最少进行多少个操作才能够把这个柱状图变成屋顶的形状。

【输入输出样例】

样例1

样例2

column.in

column.out

column.in

column.out

4

1 1 2 3

3


5

4 5 7 2 2

4

样例的解释: 例一升高2,3,4号柱子一个单位高度是操作最少的方法之一,最高处为第四个柱子。例二降低第三根柱子三个高度,升高第四个柱子一个高度。最高处为第2个柱子。


【数据范围】

30%的数据满足:1<=N<=100

60%的数据满足:1<=N<=5000

100%的数据满足:1<=N<=1000001<=hi<=109

这道题考试的时候想打暴力来着……结果发现自己没有什么能够保证正确性的暴力,所以就瞎写了一通交了上去然后爆零。

具体该怎么做呢?我们首先思考60分的算法。我们可以选择枚举每一个点作为最高点,不过之后该怎么做呢?我们发现,对于一个点作为最高点的情况,我们如果改变这个点的高度,那么其他的点也都会随之移动,比其矮的会增加一个高度(增加一个操作),比其高的会少减少一个高度(减少一个操作),这样的话每次移动高度所带来的影响必然保证其操作次数和高度成一个单峰函数(只有一个最小值,虽然说可能有很多点取到了最小值)

那么我们就可以这样进行暴力枚举之后三分高度计算花费,时间复杂度O(n^2logn)

之后我们考虑100分做法。我们发现,枚举每个点是必须的,三分高度也是必须的。那么唯一的突破口在于如何统计答案。我们考虑到,对于当前枚举成为最高点的点i,在其左边的点,必然有h[i] - i == h[j] - j,而对于在其右边的点,则必有h[i] + i == h[j] + j。那么我们就可以这样做:将每个点(i)按照h[i]+i,h[i]-i分别进行排序,之后考虑在枚举每个点成为最高点的时候,在维护其左边的一组(存h[i]-i)的数据中找出h[j]+j == h[i]+i的那个点所在的位置,之后分别统计两侧的点的个数和这些点的高度和,那么高度和-点的个数×当前值(h[i]-i)的绝对值就是你这个区间之内的答案,而另一侧的区间就是找(h[i]+i)。这个可以使用两个树状数组分别维护左右两端的情况。

上面的式子可以自己推一下,简单的来说就是对于一个高度为k的柱子,你要将其修改为一个合适的值h[j],而h[j]的值是有的,就是h[i]+i-j(当然在另一侧也一样),所以我们只要将原数全部求和之后相减即可。

说句实话确实相当难理解……

看一下代码。

#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
#include<set>
typedef long long ll;
#define pr pair<ll,int>
#define mp make_pair
#define fi first
#define sc second
#define lowbit(x) x & (-x)
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
const int M = 100005;
const int INF = 1e9+7;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-') op = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        ans *= 10;
        ans += ch -'0';
        ch = getchar();
    }
    return ans * op;
}

int n,rel,rer,h[M],rkl[M],rkr[M];
ll ans;

struct pillar
{
    int pos,v;
    bool operator < (const pillar &g) const
    {
        return pos < g.pos;
    }
}pl[M],prr[M];//pl,pr分别记录在当前枚举的柱子左右的柱子 

struct bit
{
    ll t[M];
    int cnt[M];
    void update(int x,int pos,int num)//树状数组修改(分别维护和和点的个数)
    {
        while(x <= n) t[x] += pos,cnt[x] += num,x += lowbit(x);
    }
    pr sum(int x)//树状数组求和
    {
        ll tmp = 0,ret = 0;
        while(x) tmp += t[x],ret += cnt[x],x -= lowbit(x);
        return mp(tmp,ret);
    }
    pr query(int kl,int kr)//求一段区间的和
    {
        if(kl > kr) return mp(0ll,0);
        pr l = sum(kl-1),r = sum(kr);
        return mp(r.fi - l.fi,r.sc - l.sc);
    }
}rig,lef;

// Right : h[j] + j = h[i] + i 
//Left: h[j] - j = h[i] - i

int find(pillar *p,int val)//二分查找,找到对应数组中的h[j]+j == h[i]+i的值的位置
{
    int l = 1,r = n,ret = 0;
    while(l <= r)
    {
        int mid = (l+r) >> 1;
        if(p[mid].pos > val) r = mid - 1;
        else ret = mid,l = mid+1;
    }
    return ret;
}

ll solve(int x,int height)//计算一次的花费
{
    ll ret = abs(height - h[x]),ps = height - x;//初始值是一开始的花费,后面的是当前的“h[i]-i”
    int pos = find(pl,height-x);//找到位置
    pr tmp = lef.sum(pos);//先求这个位置左边的和
    ret += ps * tmp.sc - tmp.fi;//计算答案(见上文)
    tmp = lef.query(pos+1,n);//右边的和
    ret += tmp.fi - ps * tmp.sc;//同上
    
    pos = find(prr,height+x),ps = height + x;//以下操作是计算h[i]+i == h[j]+j,即当前枚举的点右边的点,与上面同理
    tmp = rig.sum(pos);
    ret += ps * tmp.sc - tmp.fi;
    tmp = rig.query(pos+1,n);
    ret += tmp.fi - ps * tmp.sc;
    return ret;
}

int main()
{
    n = read();
    rep(i,1,n) h[i] = read();
    rep(i,1,n) pl[i] = (pillar){h[i]-i,i},prr[i] = (pillar){h[i]+i,i};//将柱子按照h[i]+i,h[i]-i分别储存,相当于记录在左/右两边的柱子
    sort(pl+1,pl+1+n),sort(prr+1,prr+1+n);//按照h[i]+i(权值)进行排序
    ans = 1ll << 60;
    
    rep(i,1,n) rkl[pl[i].v] = i,rkr[prr[i].v] = i;//记录每个点原来的位置
    rep(i,1,n) rig.update(rkr[i],h[i]+i,1);//向树状数组里添加元素
    rep(i,1,n)
    {
        rig.update(rkr[i],-h[i]-i,-1);//统计之前删除自己
        int l = max(i,n-i+1),r = INF;
        while(r-l > 1)//进行三分高度
        {
            int mid = (l+r) >> 1;
            rel = solve(i,mid-1),rer = solve(i,mid);
            if(rel >= rer) l = mid;
            else r = mid;//取最优解
        }
        ans = min(ans,solve(i,l));
        ans = min(ans,solve(i,r));//在最后两个端点处选解
        lef.update(rkl[i],h[i]-i,1);//这个点的枚举已经过去了,它将成为将来枚举的点的左边的点,压到维护左边的树状数组中
    }
    printf("%lld\n",ans);
    
    return 0;
}

 

posted @ 2018-09-16 23:59  CaptainLi  阅读(808)  评论(0编辑  收藏  举报