NOIP2013 D1

T1 转圈游戏

题目描述:

n 个小伙伴(编号从 0 到 n-1)围坐一圈玩游戏。按照顺时针方向给 n 个位置编号,从0 到 n-1。最初,第 0 号小伙伴在第 0 号位置,第 1 号小伙伴在第 1 号位置,……,依此类推。游戏规则如下:每一轮第 0 号位置上的小伙伴顺时针走到第 m 号位置,第 1 号位置小伙伴走到第 m+1 号位置,……,依此类推,第n − m号位置上的小伙伴走到第 0 号位置,第n-m+1 号位置上的小伙伴走到第 1 号位置,……,第 n-1 号位置上的小伙伴顺时针走到第m-1 号位置。
现在,一共进行了 10^k轮,请问 x 号小伙伴最后走到了第几号位置。

输入输出格式:

输入格式:

输入文件名为 circle.in。
输入共 1 行,包含 4 个整数 n、m、k、x,每两个整数之间用一个空格隔开。

输出格式:

输出文件名为 circle.out。
输出共 1 行,包含 1 个整数,表示 10
k 轮后 x 号小伙伴所在的位置编号。

输入输出样例:

输入样例#1:
10 3 4 5
输出样例#1:
5

说明:

对于 30%的数据,0 < k < 7;
对于 80%的数据,0 < k < 10^7;
对于 100%的数据,1 < n < 1,000,000,0 < m < n,1 ≤ x ≤ n,0 < k < 10^9。

题解:

看到数据范围,k<10^9 , 所以线性的解法是过不了的,所以自然就想到logn的复杂度,自然就是快速幂。
我们再来分析走到位置的规律:
1、先将原问题转化:有n个小盆友,设最开始小盆友在第k(0<=k<=n-1)个位置,每次跳x个位置,跳了b步,问跳到了哪里。
2、举出一两个例子,例如有5个小盆友,那么编号就是从0~4,设一开始编号3,每次跳两步,则:
跳1次,跳到0;跳2次,跳到2;跳3次,跳到4;以此类推。。。
然后我们发现,小盆友最后跳到的位置就是(k+x*b)%n , 所以按照这个规律进行快速幂即可,然后再注意一些细节问题就可以AC了,此题还是不难的。

代码:

#include<bits/stdc++.h>
using namespace std;

#define ll long long
ll n,m,k,x; 

int main(){
    cin>>n>>m>>k>>x;
    ll base=10,res=1;
    while(k){
        if(k&1) res=(1ll*res*base)%n;
        base=(1ll*base*base)%n;
        k>>=1;
    }
    res=1ll*res*m%n;
    res=(res+x)%n;
    cout<<res<<endl;
    return 0;
}

T2 火柴排队

题目描述

涵涵有两盒火柴,每盒装有 n 根火柴,每根火柴都有一个高度。 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为: ∑(ai-bi)^2
其中 ai 表示第一列火柴中第 i 个火柴的高度,bi 表示第二列火柴中第 i 个火柴的高度。
每列火柴中相邻两根火柴的位置都可以交换,请你通过交换使得两列火柴之间的距离最小。请问得到这个最小的距离,最少需要交换多少次?如果这个数字太大,请输出这个最小交换次数对 99,999,997 取模的结果。

输入输出格式

输入格式:

输入文件为 match.in。
共三行,第一行包含一个整数 n,表示每盒中火柴的数目。
第二行有 n 个整数,每两个整数之间用一个空格隔开,表示第一列火柴的高度。
第三行有 n 个整数,每两个整数之间用一个空格隔开,表示第二列火柴的高度。

输出格式:

输出文件为 match.out。
输出共一行,包含一个整数,表示最少交换次数对 99,999,997 取模的结果。

输入输出样例

输入样例#1:

4
2 3 1 4
3 2 1 4

输出样例#1:

1

输入样例#2:

4
1 3 4 2
1 7 2 4

输出样例#2:

2

说明

【输入输出样例说明1】
最小距离是 0,最少需要交换 1 次,比如:交换第 1 列的前 2 根火柴或者交换第 2 列的前 2 根火柴。
【输入输出样例说明2】
最小距离是 10,最少需要交换 2 次,比如:交换第 1 列的中间 2 根火柴的位置,再交换第 2 列中后 2 根火柴的位置。
【数据范围】
对于 10%的数据, 1 ≤ n ≤ 10;
对于 30%的数据,1 ≤ n ≤ 100;
对于 60%的数据,1 ≤ n ≤ 1,000;
对于 100%的数据,1 ≤ n ≤ 100,000,0 ≤火柴高度≤ maxlongint

题解:

我们先将公式(ai-bi)^2拆开,
(ai-bi)^2=ai^2+bi^2-2ai*bi。
因为拆开之后前面两项确定,所以要使原式最小,就要使第三项最大。
经过分析,让a数组中最小的和b数组中最小的放一起第三项最大。
为什么?:
设四个数 a<b c<d
根据以上猜测,则 ac+bd 一定是最大的。利用反证法。
ac+bd 不是最大的,那么一定有比它更大的,只有 ad+bc
ac+bd<ad+bc
acad<bcbd
a(cd)<b(cd)//cd<0
a>b
所以当排好序后a数组为升序,
b数组交换后为升序时结果最小。
拿样例举例
a = 1 3 4 2
b = 1 7 2 4
首先对a序列进行排序,b序列相应的元素也要交换位置
于是,原序列变成了
a = 1 2 3 4
b = 1 4 7 2
即求b数组逆序对的个数即为答案。

代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>

using namespace std;

#define MAX 100010
#define INF 2147483647

int arr[MAX];
long long n,j,c,s,sum=0;
int L[MAX/2];//合并时存放左区间
int R[MAX/2];//合并时存放右区间 

struct Node
{
      int s,w;
}a[MAX],b[MAX];

bool cmp1(Node x,Node y)
{
       return x.s<y.s;
}

void mergesort(int l,int r)//归并排序求逆序对 
{
       if(l==r)      //只有一个元素,显然有序 
          return;

       int mid=(l+r)/2;

       mergesort(l,mid);
       mergesort(mid+1,r);

       int ln=mid-l+1;//左区间数字个数
       int hl=1,rl=1;//左区间和右区间的头标记

       memcpy(L+1,arr+l,(mid-l+1)*4);
       memcpy(R+1,arr+mid+1,(r-mid)*4);
           //上面这两句可以等效替换成下面的语句,时间上几乎没有区别 
       /*
       for(int i=l;i<=mid;++i)
          L[i-l+1]=arr[i];
       for(int j=mid+1;j<=r;++j)
          R[j-mid]=arr[j];
       */

       L[mid+2-l]=R[r+1-mid]=INF; //最后置为极大值,防止下方合并时出现错误 

       for(int i=l;i<=r;++i) //合并左右两个已经有序的序列 
       {
                 if(L[hl]<R[rl])//如果左区间的头比右区间的头更小 
                 {
                         arr[i]=L[hl++];
                         ln--;
                 }
                 else           //反之,则存在逆序对 
                 {
                         arr[i]=R[rl++];
                         sum+=ln;
                         sum%=99999997;
                 }
       }
}

int main()
{
      cin>>n;

      for(int i=1;i<=n;++i)
      {
         cin>>a[i].s;
         a[i].w=i;
      }

      for(int i=1;i<=n;++i)
      {
         cin>>b[i].s;
         b[i].w=i;
      }

      sort(&a[1],&a[n+1],cmp1);//对a进行排序,方便算b的位置 
      sort(&b[1],&b[n+1],cmp1);//对b进行排序,方便求逆序对 

      for(int i=1;i<=n;++i)//把数字放好,准备求逆序对个数 
      {
               arr[a[i].w]=b[i].w;  //这步自己拿笔写一写更好理解 
      }

      mergesort(1,n);//归并排序 

      cout<<sum<<endl;//输出结果 

      return 0;
}

/* 
归并排序:
          每次把当前序列分为左右两个有序序列,将其逐步合并
          得到结果。
          还是举样例
             1 4 7 2
          1.对于(1 4 7 2) 
               左区间(1 4) 右区间(7,2)
             2.对于(1 4)
                 左区间(1) 右区间(4)  
                 左右区间已经有序
                    合并两个区间,(1,4)
             3.对于(7 2)
                 左区间(7) 右区间(2)
                 左右区间已经有序
                    合并两个区间,(2,7)
                    产生了逆序对1对
             左右区间已经有序
             合并结果为(1 2 4 7)
             产生了逆序对1对
           因此,在该过程中,总共产生了两对逆序对,所以答案为2 
           至于如何统计逆序对个数,方法如下:
               就拿样例的最后一步合并
               左区间(1 4)
               右区间(2 7)
               因为左右区间都是有序,因此每次比较两个区间的头元素
                  1.左边头元素(1)小于右边头元素(2),
                    因为是升序排列,显然是正常的,不计算逆序对个数
                    同时左边元素个数-1,排序结果变为(1)
                  2.左边头元素(4)大于右边头元素(2),
                    此时要将原位置靠后的右区间的元素放到前面, 
                    每一次放置会和左区间中还没有放置的元素产生
                    一次交换,因此,产生了逆序对的个数就是当前
                    左区间的元素个数,对于这里也就是1对
                    同时右边元素个数-1,排序结果变为(1 2)
                  3.左边头元素(4)小于右边头元素(7),
                    同1的操作,排序结果变为(1 2 4)
                  4.左边无元素了(被置为INF),INF大于右边头元素(7)
                    同2的操作,但是此时左边元素个数为0
                    排序的结果就变为了(1 2 4 7)
                  5.排序结束
           从这个合并的过程中很容易看出如何求出逆序对个数,
           我们只需要在任意一步的合并之中都执行一边这样的
           操作,并且将结果累加,就是最后的逆序对个数。                 
*/ 

T3 货车运输

题目描述:

A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。现在有 q 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。

输入输出格式

输入格式:

输入文件名为 truck.in。
输入文件第一行有两个用一个空格隔开的整数 n,m,表示 A 国有 n 座城市和 m 条道路。 接下来 m 行每行 3 个整数 x、 y、 z,每两个整数之间用一个空格隔开,表示从 x 号城市到 y 号城市有一条限重为 z 的道路。注意: x 不等于 y,两座城市之间可能有多条道路 。
接下来一行有一个整数 q,表示有 q 辆货车需要运货。
接下来 q 行,每行两个整数 x、y,之间用一个空格隔开,表示一辆货车需要从 x 城市运输货物到 y 城市,注意: x 不等于 y 。

输出格式:

输出文件名为 truck.out。
输出共有 q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。如果货车不能到达目的地,输出-1。

输入输出样例

输入样例#1:
4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3
输出样例#1:
3
-1
3

说明

对于 30%的数据,0 < n < 1,000,0 < m < 10,000,0 < q< 1,000;
对于 60%的数据,0 < n < 1,000,0 < m < 50,000,0 < q< 1,000;
对于 100%的数据,0 < n < 10,000,0 < m < 50,000,0 < q< 30,000,0 ≤ z ≤ 100,000。

题解:

首先看到这题的数据范围, n<10000 ,所以显然就是用邻接表存,但是题目又
说有重边,所以就考虑生成树,而且是最大生成树。那么是用 prim 还是 kruskal 呢?我们还是先观察一下数据范围,显而易见是选后者。我们用最大生成树重新构图后又该怎么办?分为一下两步:
1 两个点不通:这是我们喜闻乐见的,直接一个并查集上去就行了。
2 两点相通:第一反应应该是最短路,但是最短路会超时,所以考虑倍增 lca 求两点求两点之间的最小边权。
如上面操作即可 AC 了。

代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 50010;

int head[N], rest[N], to[N], w[N], tot, n, m, q, deep[N], fa[N][30], mn[N][30];

void add(int u, int v, int wi) {
    tot ++;
    to[tot] = v;
    w[tot] = wi;
    rest[tot] = head[u];
    head[u] = tot;
}

void dfs(int u) {
    for(int i = head[u] ; i ; i = rest[i]) {
        int v = to[i];
        if(v != fa[u][0]) {
            deep[v] = deep[u] + 1;
            fa[v][0] = u;
            mn[v][0] = w[i];
            dfs(v);
        }
    }
}

void seq() {
    for(int j = 1 ; j <= 20 ; j ++) {
        for(int i = 1 ; i <= n ; i ++) {
            fa[i][j] = fa[fa[i][j - 1]][j - 1];
            mn[i][j] = min(mn[i][j - 1], mn[fa[i][j - 1]][j - 1]);
        }
    }
}

struct E {
    int u, v, w;
} e[N];

bool operator < (E a, E b) {
    return a.w > b.w;
}

int acc[N];

int find(int x) {return x == acc[x] ? x : acc[x] = find(acc[x]);};

int ask(int u, int v) {
    if(find(u) != find(v)) return -1;
    int ret = 0x3f3f3f3f;
    if(deep[u] > deep[v]) swap(u, v);
    for(int i = 20 ; i >= 0 ; i --) {
        if(deep[fa[v][i]] >= deep[u]) {
            ret = min(ret, mn[v][i]);
            v = fa[v][i];
        }
    }
    if(u == v) return ret;
    for(int i = 20 ; i >= 0 ; i --) {
        if(fa[u][i] != fa[v][i]) {
            ret = min(ret, min(mn[u][i], mn[v][i]));
            u = fa[u][i];
            v = fa[v][i];
        }
    }
    return min(ret, min(mn[u][0], mn[v][0]));
}


int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1 ; i <= m ; i ++) {
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
    }
    for(int i = 1 ; i <= n ; i ++) acc[i] = i;
    sort(e + 1, e + m + 1);
    for(int i = 1 ; i <= m ; i ++) {
        if(find(e[i].u) != find(e[i].v)) {
            acc[find(e[i].u)] = find(e[i].v);
            add(e[i].u, e[i].v, e[i].w);
            add(e[i].v, e[i].u, e[i].w);
        }
    }
    memset(mn, 0x3f, sizeof(mn));
    dfs(1);
    seq();
    scanf("%d", &q);
    for(int i = 1, u, v ; i <= q ; i ++) {
        scanf("%d%d", &u, &v);
        printf("%d\n", ask(u, v));
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值