标签存档: 解题报告

[POJ1771] Elevator Stopping Plan

新浪的 spamer 越来越多,很早就想搬到独立的 wordpress 上去,但是一直没顾上。只好先在sina这里凑合着了。

Elevator Stopping Plan
Time Limit: 1000MS  Memory Limit: 30000K  Special Judge

Description

ZSoft Corp. is a software company in GaoKe Hall. And the workers in the hall are very hard-working. But the elevator in that hall always drives them crazy. Why? Because there is only one elevator in GaoKe Hall, while there are hundreds of companies in it. Every morning, people must waste a lot of time waiting for the elevator.

Hal, a smart guy in ZSoft, wants to change this situation. He wants to find a way to make the elevator work more effectively. But it’s not an easy job.

There are 31 floors in GaoKe Hall. It takes 4 seconds for the elevator to raise one floor. It means:

It costs (31-1)*4=120 seconds if the elevator goes from the 1st floor to the 31st floor without stop. And the elevator stops 10 second once. So, if the elevator stops at each floor, it will cost 30*4+29*10 = 410 seconds (It is not necessary to calculate the stopping time at 31st floor). In another way, it takes 20 seconds for the workers to go up or down one floor. It takes 30*20 = 600 seconds for them to walk from the 1st floor to the 31st floor. Obviously, it is not a good idea. So some people choose to use the elevator to get a floor which is the nearest to their office.

After thinking over for a long time, Hal finally found a way to improve this situation. He told the elevator man his idea: First, the elevator man asks the people which floors they want to go. He will then design a stopping plan which minimize the time the last person need to arrive the floor where his office locates. For example, if the elevator is required to stop at the 4th, 5th and 10th floor,the stopping plan would be: the elevator stops at 4th and 10th floor. Because the elevator will arrive 4th floor at 3*4 = 12 second, then it will stop 10 seconds, then it will arrive 10th floor at 3*4+10+6*4 = 46 second. People who want to go 4th floor will reach their office at 12 second, people who want to go to 5th floor will reach at 12+20 = 32 second and people who want to go to 10th floor will reach at 46 second. Therefore it takes 46 seconds for the last person to reach his office. It is a good deal for all people.
Now, you are supposed to write a program to help the elevator man to design the stopping plan,which minimize the time the last person needs to arrive at his floor.

Input

The input consists of several testcases. Each testcase is in a single line as the following:
n f1 f2 … fn
It means, there are totally n floors at which the elevator need to stop, and n = 0 means no testcases any more. f1 f2 … fn are the floors at which the elevator is to be stopped (n <= 30, 2 <= f1 < f2 … fn <= 31). Every number is separated by a single space.

Output

For each testcase, output the time the last reading person needs in the first line and the stopping floors in the second line. Please note that there is a summary of the floors at the head of the second line. There may be several solutions, any appropriate one is accepted. No extra spaces are allowed.

Sample Input

 3 4 5 10
 1 2
 0

Sample Output

 46
 2 4 10
 4
 1 2

Source

Asia Guangzhou 2003

Solution

很久不更新了,写个水水的解题报告充数。。。这是最近做的算法习题,网上看到这题的题解都是二分+贪心的,这里提供一个dp解法。

理解题意的时候有一点需要特别注意:题目所描述的整个过程是“并行”的。所以所有人都到达各自楼层的用时只与最晚到达的人有关。

首先,由于去各楼层乘客的具体数目对结果没有影响,为表述方便,我们假设每个目标楼层只有一个人要去,并且把各个目标楼层与要去该楼层的那个乘客对应。下面在说“电梯里还剩i个人”的时候,就是在说“电梯里的乘客还要去i个楼层”。

粗略的阶段划分和状态表示还是很简单的。

电梯从第1层开始层层上升,每层都看做一个阶段,任意时刻的状态都可以由“电梯在几楼”和“电梯上都有谁”这两个参数唯一确定。初始状态就是“电梯在1楼”和“所有人都在电梯上”。

在每个阶段需要做出决策,选择让电梯上的哪些人下来自己走(如果没人下来就表示电梯在这层不停)。每个决策发生后,原来电梯里的人被分成两拨:一拨留在电梯上继续上升,另一拨离开电梯开始爬楼锻炼身体。要求某个状态下电梯里的所有人到达各自楼层所需的最短时间,只需要找到一个最优的决策,使得上述两拨人中最晚到达的人尽早到达。

即,若设决策后留在电梯里的人全部到达各自楼层需要时间 T1,离开电梯的人全部到达需要时间 T2,则要求的就是 min{ max(T1, T2) }。其中 T1 可由“当前层数+1”和“决策后剩下的人”确定的状态得到;T2 则是下电梯的人中走的最远的那位所花的时间。

按照上述想法很容易列出转移方程。但是“电梯上都有谁”这一参数有 2^n 种取值,整个算法的复杂度因此能到达令人发指的O(m*2^2n),对于m=31,n=30的数据规模这是不可接受的。

我们需要设法减少需要考虑的状态数目。下面给出两个引理:

1. 电梯决定停在第k层时:要去 1..k 层的人应选择在这时下电梯,这样一定可以得到当前决策下的一个最优解。如图:

2. 电梯在第k层时,若要去 k+r 层的人选择在这时下电梯,则:要去k+1..k+r-1层的人也应选择在此时下电梯,这样一定可以得到当前决策下的一个最优解。如图:
http://upload.tomtung.com/img/poj1771_2.png
以上两点很容易证明。由这两点可以得到一个很简单但足以解决问题的结论:

无论电梯停在哪一层,若要去第 k 层的人选择在这时下电梯,则:所有要去低于k层(第1..k-1层)的人也应选择在此时下电梯,这样一定可以得到当前决策下的一个最优解。如图:

http://upload.tomtung.com/img/poj1771_3.png

也就是说,如果把初始时的 n 名乘客按照各自要去的层数从(注意此顺序与输入相反)排列,并依此编号为第 1、2、3…n 个人,第 i 个人要去第 f[i] 层(f[1]>f[2]>…>f[n]),那么可以认为任意时刻电梯里乘客的编号都是 1, 2,..,x 这样一个连续序列。也就是说,对于电梯里的人我们只需要考虑编号为 1, 2, 3 或 1, 2, 3, 4, 5 这样连续排列的情况,而无需考虑 1, 2, 4(缺3)或2, 3, 4(缺1)这样的情况。
http://upload.tomtung.com/img/poj1771_4.png
这样一来,每个状态都能由两个数[i,j]来表示:电梯在第i层,电梯里有j个人,即要去楼层最高的第1,2,..,j个人。

下面给出转移方程:

f[i,j]表示电梯在第i层,电梯上有要去楼层最高的j个人时,电梯上的人全部到达各自楼层所需的最短时间

f[i,j] = min{ max(t1, t2) } (0<=k<=j)

t1 = f[i+1, k] + 电梯停留时间 + 电梯上升一层所用时间

t2 = max{ |d[l] – i| * 人爬一层楼所用时间 } ( k+1<=l<=j )

边界条件、最优解的构造方法以及其它细节问题不再赘述,详见代码。复杂度O(m*n^2)。代码中其实还有优化的空间,但已经是0ms过的,没必要了。

#include <iostream>
using std::cin;
using std::cout;
using std::endl;

#include <cstring>
using std::memset;

#include <algorithm>
using std::max;

#include <cmath>
using std::abs;

#include <limits>
using std::numeric_limits;

#include <vector>
using std::vector;

const int maxN = 30, maxF = 31;
const int ve = 4, st = 10, vw = 20; // 电梯上一层所需时间;电梯停一层所需时间;人走一层所需时间

int n, f[maxN + 1];

bool input()
{
    cin >> n;
    if (n==0) return false;

    // 注意:f[1..n]中楼层数从高到底排列
    for (int i = n; i>=1; --i)
        cin >> f[i];

    return true;
}

int dp[maxF + 1][maxN + 1], nextJ[maxF + 1][maxN + 1];

// 现在电梯在第currF层,第L到第R人离开电梯
// 函数返回这些离开电梯的人中最晚到达目的楼层所需的时间
int tLeave(int currF, int l, int r)
{
    if (l>r)  return 0;
    // 仅需考虑两端
    return max(abs(currF-f[l]), abs(currF-f[r])) * vw;
}

// 现在电梯在第i层,电梯里本来有j个人,在要下电梯的人离开后还剩jj个人
// 函数返回这些留在电梯里的人中最晚到达目的楼层所需的时间
int tStay(int i, int j, int jj)
{
    // 没人下电梯
    if (j==jj)
        return dp[i+1][jj] + ve;
    // 所有人都离开电梯
    else if (jj==0)
        return 0;
    // 第1层不计算电梯停留时间
    else if (i==1)
        return dp[i+1][jj] + ve;
    //普通情况
    else
        return dp[i+1][jj] + ve + st;
}

void calculate()
{
    // 边界:电梯在顶楼时所有人都必须下电梯
    int topFloor = f[1];
    for (int j = 1; j<=n; ++j)
        dp[topFloor][j] = tLeave(topFloor,1,j);

    for (int i = topFloor - 1; i>=1; --i)
        for (int j = 1; j<=n; ++j)
        {
            dp[i][j] = numeric_limits<int>::max();
            for (int jj = 0; jj <= j; ++jj)
            {
                // 取离开电梯的人和留下的人中的最晚到达者
                int tmp = max(tStay(i,j,jj),tLeave(i,jj+1,j));
                if (dp[i][j] > tmp)
                {
                    dp[i][j] = tmp;
                    nextJ[i][j] = jj;
                }
            }
        }
    cout << dp[1][n] << endl;
}

void rebuildSolution()
{
    vector<int> stops;
    int j = nextJ[1][n], topFloor = f[1];
    for (int i = 2; i<=topFloor; ++i)
        if (nextJ[i][j]!=j)
        {
            stops.push_back(i);
            j = nextJ[i][j];
            if (j==0) break;
        }

    cout << stops.size();
    for (int i = 0; i!=stops.size(); ++i)
        cout << ' ' << stops[i];
    cout << endl;
}

void solve()
{
    memset(dp,0,sizeof(dp));
    memset(nextJ,0,sizeof(nextJ));
    calculate();
    rebuildSolution();
}

int main()
{
    while (input())
        solve();
}

[BHOJ10235] 窗口取数

http://upload.tomtung.com/img/sliding-window.jpg

窗口(超级版)

时间限制:5000 ms 内存限制:65535 KB

描述

有一串整数在排队……

有N个整数,你有一个可以框住M个连续段整数的木框,现在你想知道,对于这个队列中任意的连续M个整数,最大和最小的整数是哪个?

例:(M大小的窗口向右滑动)

1 2 3 2 1   M=2
1 2    MAX 2  MIN 1
2 3    MAX 3  MIN 2
3 2    MAX 3  MIN 2
2 1    MAX 2  MIN 1

最后需要输出的是两行:

1 2 2 1
2 3 3 2

输入

第一行包含一个整数T,表示有T组测试数据;
以下每组测试数据格式:
第一行包含2个整数N,M表示有N个整数在排队,取连续M个整数。
第二行包含N个整数。
其中N不大于1,000,000,M不大于N。

输出

按照题目描述格式输出结果,第一行为MIN,第二行为MAX。

样例输入

1
5 2
1 2 3 2 1

样例输出

1 2 2 1
2 3 3 2

提示

注意数据规模!

问题来源

软件学院07级数据结构第二次测试

题解

很久不考虑算法问题了的说……当然也好久不写解题报告了。上面这题让我体会到久违了的思考的乐趣(i.e.我废掉好久了>_<)

最显然的解法是利用平衡树始终保持木框内M个数的有序状态,大体是O(NlogM)的复杂度。这个不用多说。

问题是:能否找到O(N)的解法?在读下去之前你可以好好想想。

我纠结了好久,终于……也没想出来- – 尽管想出很多优化,终究还是不能达到要求。网上搜了下,看到 CS大牛csdn 上有人给出了一个解法(你也可以在读完全文后再回头看这一段):

是可以到o(n);

编程之美上有一个类似的问题:”队列中取最大值操作问题”;
实际上窗口移动就相当于对队列做了一次出队与入队操作,所以lz这道题可以套用该解法;

书上是使用两个栈来模拟队列,假设为分别A,B;
1)当入队的时候,push A;
2)当出队的时候,
a)若B非空,pop B,
b)若B为空,则先将A中的元素依次pop并push到B,再pop B;
这样,就使用两个栈达到了队列的功能;

同时,对于单个栈,由于pop,push都是在栈顶进行的,所以每个栈都可以方便地维护自己的最
大值与最小值在栈内的索引;
以最大值为例;
用max_idx保存栈内最大值的索引;
另使用一个跟栈的最大长度一样的数组idx,idx[i]表示栈的索引范围在0到i-1的元素中的最
大值的索引为idx[i];
1) 当push的时候,比较栈顶元素与栈的已保存的最大值,
a) 若栈顶元素大于已保存的最大值,那么idx[top]=max_idx,max_idx=top;
b) 若栈顶元素不大于已保存的最大值,那么idx[top]=max_idx(书上是idx[top]=-1);
2) 当pop的时候,max_idx=idx[top](书上是先比较top与max_idx,若top==max_idx,则
max_idx=idx[top]);

具体到这个题目;
可以使用两个长度为m的栈来模拟窗口;
1)将前m个元素依次push到栈A;
2)移动窗口就相当于分别进行pop B 与 push A操作;
3) 窗口的最大值==max[A.items[A.max_idx]),B.items[B.max_idx])];

由于每个元素最多进入两栈各一次;
所以总的复杂度是o(n)的;

这个解法的确可行,但描述仍不直观。为什么两个栈暧昧地眉来眼去一番,就在O(N)时间内把问题解决了呢?褪去实现上的种种细节,从更高的角度观察这个算法,它的思路是怎么样的呢?

和冬冬讨论了半天,似乎找到了一个比较靠近此算法根本动机的理解方向。如下:

一、简化

此问题中,框的左端和右端同时在向右移动。这时,要在任意时刻用O(1)的时间获得框内最小值(最大值同理,故仅以最小值为例)并不容易。但是,如果假设这个框是可伸缩的,将其一端固定,仅移动另外一端,任意时刻能否在O(1)的时间内获得最小值呢?

稍加思考,就发现要找到这样的算法是很容易的(事实上在我最初考虑时,就是试图在这样一个算法的基础上进行优化的)。

假如这个框是左端固定,右端不断向右移动的(称之为左定右动框)。对于框内的每个数,有一个对应指针指向框内它左边(包括自身)所有数中最小者。这样,框内最小值就是框内最右端元素对应的指针所指向的元素。如在下图中,最小元素就是右端7对应指针所指向的元素6。

http://upload.tomtung.com/img/bhoj-10235_1.png

min = 6

当框的右端向右扩展时,可以通过递推获得新元素对应指针应指向的元素。以上图为例。加入2时,由7对应的指针可知原框中最小值为6,而6>2,所以新元素2对应的指针应指向自己。

http://upload.tomtung.com/img/bhoj-10235_2.png

min = 2

加入8时,由2的指针知原框中最小值为2,而2<8,所以新元素8对应指针应指向2的指针所指向的元素2。

http://upload.tomtung.com/img/bhoj-10235_3.png

min = 2

其余如法炮制。

http://upload.tomtung.com/img/bhoj-10235_4.png

min = 2

可见,在框左端固定、右端不断扩展时,任意时刻都能在常数时间内确定框内元素的最小值。(不仅如此,即使需要让右端向左收缩,也能在常数时间内,通过最右端元素指针获得得到收缩后框内所剩元素的最小值。) 整个过程演示如下(先扩张后收缩):

http://upload.tomtung.com/img/bhoj-10235_5.gif

演示中的收缩操作在当前问题中并无必要。演示出来是为了说明无论右端如何移动,只要左端不动,就可以在任意时刻立刻得到最小值。同时也是为了与下面一个演示保持一致。

如果一个框右端固定,左端收缩(称之为右定左动框),也可以在任意时刻花费常数时间获得框内的最小元素。过程和上面左右对称。具体地说,每个元素对应一个指针,指向框内它右边(包括自身)所有数中最小者。首先需要从右到左计算出各个元素对应的指针。这个过程类似于将框的左端由右向左扩展。计算完毕后就可以在收缩左端的过程中在常数时间内获得最小元素了。

演示如下(先扩张后收缩,可以看到完全和上面对称):

http://upload.tomtung.com/img/bhoj-10235_6.gif

对于当前问题,在“扩张”过程中并不需要求最小值,而仅仅需要计算出指针来为收缩过程作准备。这里在全过程中标注出最小值是为了说明任意时刻都有能力得到它,也为了与上一个演示保持一致。

上面拉拉杂杂说了一堆,只为了说明:如果框的一端固定,仅移动另外一端,则在此过程中可以仅花费常数时间获得框内的最小元素。针对左定右移、右定左移两种情况的算法是彼此对称的。

最后顺带提一点实现细节,希望不会影响你对整体的理解。上面所使用的指针也可以用索引号来代替,这里使用指针是为了显得更形象。事实上,不保存索引或指针而直接保存最小的“值”也是可行的,但是并不推荐。设想,如果每个元素不是数而是四五米长的字符串(>_<),偏序关系使用字符串长度的比较,那么如果不保存指针/索引而保存值,所带来的元素复制开销恐怕不是你想要的。

二、推广

怎样把上面只允许一端移动的解法推广到同时允许两端移动呢?

一种想法是,将以上二者合二为一。具体地说,将框内的数看成左右两部分,左边一部分看成右定左动的,右边一部分看成左定右动的。这样,在左边收缩、右边扩张的过程中,左右两部分都可以在常数时间内得到最小元素。取两个最小元素中更小者,即为整个框中的最小元素。

OK,这就是O(N)算法的基本思路了。回到原题目,下面看一个例子。

假设我们要处理的是这么一串数:

3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3

框的大小为5。

首先,把最先被框住的5个数看成被一个右定左动的框框住,这5个数右边看成是一个空的左定右动框。当然,首先需要计算出开始这5个元素对应的指针:

http://upload.tomtung.com/img/bhoj-10235_7.gif

接下来,左边的右定左动框收缩,右边的左定右动框扩张。在此过程中,框在框中的M个元素的最小者可以在常数时间内获得。

http://upload.tomtung.com/img/bhoj-10235_8.gif

到左边部分收缩至空时就没有办法继续收缩了。怎么继续这个过程呢?

解决方法是,将右端含有M个元素的左定右动框重新处理为右定左动框,并在右边再放上一个空左定右动框:

http://upload.tomtung.com/img/bhoj-10235_9.gif

接下来继续这个过程就行了。下面是全过程的演示:

http://upload.tomtung.com/img/bhoj-10235_X.gif

你可能会对每次将右边的左定右动框重新处理为左边的右定左动框(有点晕- -)时计算指针造成的额外开销存有疑虑。然而,每次重新计算时需要处理M个元素的指针,每隔M个元素才会进行一次这样的处理,N/M*M仍然为N,并不会升高复杂度的阶。换句话说,每个元素至多被重新计算两次指针,所以总体复杂度仍然为O(N)。

以上就是整个解法。

三、关于栈

我们可以把右定左动框看成是一个底在右顶在左的栈,左端向左扩张看成是向栈push元素,左端向右收缩看成是栈在pop元素。左定右动框亦然。前面提到,两种框上的操作是对称的;如果把它们都看成栈,则push和pop时的操作完全一致。这样就将两种框对称的操纵统一在了同一个数据结构上,使得实现起来更为简洁。

这就是对开始所引用那段算法描述的解释。

四、问题的扩展

更进一步,我们还可以使用类似的方法处理此问题的变种。如,对于一个队列,随意进行入队出队操作(相当于木框的宽度不再固定为M,其两端也不同时向右移动),求在任意时刻队列中的最小元素。或者,一个双端队列,在两端随意进行入队出队操作,求任意时刻队列中的最小元素。等等。

最后顺便一提,在搜索此题资料时发现这个问题似乎涉及到数据流的处理算法。这个方面我一无所知,没办法站在那样一个高度阐述,抱歉。

[NOIP05] 篝火晚会

篝火晚会

(fire.pas/c/cpp)

【问题描述】
佳佳刚进高中,在军训的时候,由于佳佳吃苦耐劳,很快得到了教官的赏识,成为了“小教官”。在军训结束的那天晚上,佳佳被命令组织同学们进行篝火晚会。一共有n个同学,编号从1到n。一开始,同学们按照1,2,……,n的顺序坐成一圈,而实际上每个人都有两个最希望相邻的同学。如何下命令调整同学的次序,形成新的一个圈,使之符合同学们的意愿,成为摆在佳佳面前的一大难题。

佳佳可向同学们下达命令,每一个命令的形式如下:

(b1, b2,… bm -1, bm)

这里m的值是由佳佳决定的,每次命令m的值都可以不同。这个命令的作用是移动编号是b1,b2,…… bm –1,bm的这m个同学的位置。要求b1换到b2的位置上,b2换到b3的位置上,……,要求bm换到b1的位置上。

执行每个命令都需要一些代价。我们假定如果一个命令要移动m个人的位置,那么这个命令的代价就是m。我们需要佳佳用最少的总代价实现同学们的意愿,你能帮助佳佳吗?

【输入文件】
输入文件fire.in的第一行是一个整数n(3 <= n <= 50000),表示一共有n个同学。其后n行每行包括两个不同的正整数,以一个空格隔开,分别表示编号是1的同学最希望相邻的两个同学的编号,编号是2的同学最希望相邻的两个同学的编号,……,编号是n的同学最希望相邻的两个同学的编号。

【输出文件】
输出文件fire.out包括一行,这一行只包含一个整数,为最小的总代价。如果无论怎么调整都不能符合每个同学的愿望,则输出-1。

【样例输入】
4
3 4
4 3
1 2
1 2

【样例输出】
2

【数据规模】
对于30%的数据,n <= 1000;
对于全部的数据,n <= 50000。

【题解】

至此我就算做完了NOIP05提高组的全部四道题,发现除了第一题外其它题都不水,对于我这样的水平来说很值得一做。过河的状态压缩我想了好久;本题优化方法较过河简单,但也需要动动脑子;至于那个等价表达式则是典型的ws题:思想简单(特值法+用栈求表达式的值),但实现起来却比较繁琐,细节硕多,稍不留神就会出错,我前后一共提交了5次才AC(得到的分数分别是20、30、40、90、100)。还好这些题都是自己独立搞出来的。顺带一提,等价表达式取特值时并不像很多人说的要多个,一个足矣,当然不要取太特殊的,比如取a=1,-2,-3,0这样的就很容易被强数据阴掉,但要是取个a=-5.65742它不就没治了;同时B4数据中括号不匹配的情况。

当然这都是题外话,下面言归正传。

首先需要看到的是,虽然佳佳下达的命令形式很ws,但是在求总代价的时候却完全不需要管它。显然,在佳佳下达完一系列诡异的命令后,最后有几个人离开了原来的位置,这种情况下最小代价就是几(为什么?)。看清了这一点,问题就转化为:要使得所有人满意,最少需要让几个人离开原来的位置?

我们先考虑无解的情况。把每个人看成无向图中的节点,两人相邻则连一条边。当n个人以某种次序围坐成一个圈的时候,每个节点的度一定都是2。而这种状态一定是初始状态通过若干次次序调整能达到的,即一定有解。那么无解的情况就一定是,根据同学们的希望构图后有同学的度不为2。这样,我们只需要构图,然后看是否所有节点的度都为2就可以判断是否有解了。

如果有解,下面就需要计算最小代价了。对于我们构得的图,DFS(1)后一定得到唯一的一个序列。例如1,5,3,2,4。我们现在就需要比较它和初始状态1,2,3,4,5,看有几个人离开了原来的位置。但这个序列实际代表的是一个环,而且方向正反有两种(即1,3,5,2,4和4,2,5,3,1应该是等价的),我们就需要把初始序列正反分别“转”N次(即1,2,3,4,5; 5,1,2,3,4; 4,5,1,2,3; 3,4,5,1,2; 2,3,4,5,1 以及 5,4,3,2,1; 1,5,4,3,2; 2,1,5,4,3; 3,2,1,5,4; 4,3,2,1,5)和DFS得到的序列比较,看其中最少有几个位置上的人编号不相同,就得到了我们要求的最小代价。

DFS是O(N)的;旋转可以通过指针来实现,所以是O(1)的;每次比较是O(N)的,共进行2N次比较。因此总的复杂度是O(N^2)的。期望得分为30分(我没写过这方法哈)。

进行很多次旋转,每次都需要比较,这种方法实在太慢了,是整个算法的瓶颈。怎么改进呢?我们发现转来转去不管怎么转,任意两个人之间的相对位置关系在这过程中都不会变。于是想到做差。

1 5 3 2 4
- 1 2 3 4 5
————-
0 3 0 3 4  (如果差小于0则加上N=5)

这表示序列1,5,3,2,4不转动时1,3两个人在原来的位置上,转动3个位置后5和2两个人在原来的位置上,转动4个位置后只有4一个人会在原来的位置上。这就是说,1,5,3,2,4与1,2,3,4,5在旋转后最多有2个位置上的人编号相同,即最少有3个位置上的人编号不相同。同理:

1 5 3 2 4
- 5 4 3 2 1
————-
1 1 0 0 3  (如果差小于0则加上N=5)

1,5,3,2,4与5,4,3,2,1在旋转后最少有3个位置上的人编号不相同。

取其中的较小者(例子没举好,两个值相同了)为3,即最后要求的最小总代价为3。问题就算解决了。

算法流程总结如下:

  1. 根据输入构图,要求每个节点度只能为2,否则无解;
  2. dfs得到一个序列seq表示符合条件的环
  3. (下标从1开始)
    令a[i]=seq[i]-i(若小于0则+N)
    a中最多有x个相等的值
    令b[i]=seq[i]-(N+1-i)(若小于0则+N)
    a中最多有y个相等的值
  4. n-max(x,y)即为所求

改进后的算法复杂度为O(N),实现起来很简单。期望得分为100。

#include <iostream>
#include <fstream>
#include <cassert>
using namespace std;
long N,seq[50001],p=1;
class{
	private:
		long nbr[50001][3];
		bool flag[50001];
		bool isnbr(long v1,long v2){
			for(int i=1;i<=nbr[v1][0];i++)
				if(nbr[v1][i]==v2)	return true;
			return false;
		}
	public:
		bool AddE(long v1,long v2){
			if(isnbr(v1,v2))	return true;
			else if(nbr[v1][0]==2||nbr[v2][0]==2)	return false;
			else{
				nbr[v1][++nbr[v1][0]]=v2;
				nbr[v2][++nbr[v2][0]]=v1;
				return true;
			}
		}
		void dfs(long i){
			assert(!flag[i]);
			flag[i]=true,seq[p++]=i;
			for(int j=1;j<=nbr[i][0];j++){
				if(!flag[nbr[i][j]]){
					dfs(nbr[i][j]);
					break;
				}
			}
		}
}G;
long solve(void){
	long cnt1[50001]={0},cnt2[50001]={0},ans=0;
	for(int i=1,a,b;i<=N;i++){
		a=seq[i]-i;
		if(a<0)	a+=N;
		b=seq[i]-(N+1-i);
		if(b<0)	b+=N;
		if(++cnt1[a]>ans)	ans=cnt1[a];
		if(++cnt2[b]>ans)	ans=cnt2[b];
	}
	return N-ans;
}
int main(){
	ifstream cin("fire.in");
	ofstream cout("fire.out");
	cin >> N;
	for(int i=1,nbr1,nbr2;i<=N;i++){
		cin >> nbr1 >> nbr2;
		if(!G.AddE(i,nbr1)||!G.AddE(i,nbr2)){
			cout << -1 << endl;
			return 0;
		}
	}
	G.dfs(1);
	cout << solve() << endl;
	return 0;
}

最后再给一个在vijos里看到的算法。我自己没有实现过,有兴趣的试试吧。

见过的最牛最简单的方法……

将此题转换为冒泡排序,记录下所有交换的次数和两数间的距离,加上就行了……—__—|||

具体是这样的,我们反向思维,本来是要求一个有序数列求出成为无序数列的代价,现在我们把无序数列(即目标数列)进行冒泡排序,然后……就是这样…… 看完之后,偶巨汗……

[NOIP05]过河

过河

(river.pas/c/cpp)

【问题描述】

在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上的一串整点:0,1,……,L(其中L是桥的长度)。坐标为0的点表示桥的起点,坐标为L的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是S到T之间的任意正整数(包括S,T)。当青蛙跳到或跳过坐标为L的点时,就算青蛙已经跳出了独木桥。

题目给出独木桥的长度L,青蛙跳跃的距离范围S,T,桥上石子的位置。你的任务是确定青蛙要想过河,最少需要踩到的石子数。

【输入文件】

输入文件river.in的第一行有一个正整数L(1 <= L <=10^9),表示独木桥的长度。第二行有三个正整数S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离,及桥上石子的个数,其中1 <= S <= T <= 10,1 <= M <= 100。第三行有M个不同的正整数分别表示这M个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。

【输出文件】

输出文件river.out只包括一个整数,表示青蛙过河最少需要踩到的石子数。

【样例输入】

10

2 3 5

2 3 5 6 7

【样例输出】

2

【数据规模】

对于30%的数据,L <= 10000;

对于全部的数据,L <= 10^9。

【题解】

一看题,这无后效性和最优子结构也太明显了,随手就可以列出转移方程。令dp[i]表示处于i位置时,最少还要踩到几个石子(包括当前点)才能到达对岸。bridge[i]为点i处的石子个数(0或1)。则

当0≤i<L时:dp[i] = min{dp[i+j]} + bridge[i]   (S≤j≤T)

当i≥L时:dp[i]=0

问题的解即为dp[0]。

但是这样写完交上去最多只能得30分。直接硬搞,复杂度是O(L*(T-S))的,T-S最大为9,无关紧要,但是L的最大值则是令人发指的10^9(青蛙跳这么长距离不累死么 – -),不TLE……恐怕需要10年以后的CPU了。那怎么办呢?(思考过程也许比较繁琐,要结论请直接看倒数第5段)

数据范围往往是算法选择的最重要的提示。我们看到虽然L这么巨大,但是石子的总数却不超过100个。这说明什么?说明桥上必然有大段大段的无石子区间,而无石子区间的最大长度在极限情况下将接近10^9。即在跳跃中,青蛙经常会很郁闷地在巨长的无石子区间上跳啊跳啊,想踩个石子都踩不到。上面那个普通dp肯定会导致空白区间上大量的无必要决策而无谓耗费大量时间。要减少这种时间浪费,我们可以试图把大段的无石子区间等效转化为较短的无石子区间,从而使时间开销降至可承受范围。

首先考虑最简单的情况:S=T。这种情况下,这个诡异的青蛙只能跳固定的长度。而跳的起点是0位置,那么青蛙经过的就只有S(或者说T,一回事)的整数倍点。如果石子的位置为S的整数倍,那么青蛙就一定会踩到,否则一定踩不到。比如S=T=3,L=100,那么青蛙经过的点就为且仅为0,3,6,…,96,99。在21、27、33、66等处的石子就一定能踩到,而20、40、80、92等处的石子就踩不到。于是,当S=T时,我们只需要数位置为S整数倍的石子有几个就得到了答案,不需要进行上面说到的“等效转化”。

那如果S≠T呢?我们也从一个最简单的例子看起。假如S=5,T=6,青蛙从0位置开始跳,那么它可能到达的点是:

5,6,

10,11,12,

15,16,17,18,

20,21,22,23,24,

25,26,27,28,29,30,31,32,33,……

可以看到,这些点组成了一个个连续的区间,各相邻区间起点间的距离都是S=5,开始区间长度为2,然后长度变为3,4,然后完全连在一起,后面的位置都可达。如果桥上0~999都没有石子,1000处有一个石子,那么我们只关心青蛙是否选择跳到999,998,997,996,995,994这几个位置,因为青蛙在这段无石子区间上跳半天最后总会跳到这6个位置上,在无石子区间中不管怎么跳其实都无所谓。那么,我们完全可以无视25以后的数,把这段区间等效为0~25,以确保25以后所有位置都可达且有20~25这段连续区间来代替994~999这段等长的区间。这样,我们在这段无石子区间上的决策就大大减少了。

桥上的其他无石子区间是否可以如法炮制呢?答案是肯定的。对于以后任何一段长度大于25+6=31的无石子区间(这是采取“等效”措施的“门槛”),我们都可以把它的长度看成31。(25为什么要加上6呢?考虑一下,进入这段无石子区间后,青蛙开始往前跳的的起点,它可能不是区间本身的起点。)这样处理后,即使桥长度达到10^9,也可以在非常短的时间内出解。事实上,我们不仅可以把无石子区间长度等效为31,等效为36,121,500也都可以,只要长度比31大就行(当然“门槛”也要相应升高)。我们把这个等效区间的“最短”长度(本例中为31)称为“等效区间最短长度”。

下面推广。当S≠T时我们发现,在T不变的情况下,T-S越大(即S越小),等效区间最短长度越短。T-S不变的情况下,T越大,等效区间最短长度越长。那么对于题目给出的数据范围,S=9,T=10时(满足T-S最小且T最大)得到最大的“等效区间最短长度”为100。对于其他的S和T,我们不需要专门计算它们对应的等效区间最短长度,直接采用100这个值就可以了。

综上,若S=T,我们直接数位置能被S整除的石子个数;若S≠T,如果某无石子区间长度大于100,则等效为100,否则不变,然后再dp。

至于为什么有同学取比100小的数也AC了,我觉得(不一定对哈,没验证)应该是数据弱了。经实验,即使取20也可以AC,取10也才WA一个点。

对于等效区间最短长度的这番计算其实完全不必要。比赛时最好的方法是:取时间复杂度可接受的最大值……这样最省事。

上面可能做麻烦了,欢迎提供简明解法,谢谢。

最后说点题外话。这题我调了一晚上,郁闷死了。最后发现问题竟然是:石子位置没有按照升序排列,我却想当然这么处理了,结果一个点都过不去……

代码如下:

#include <iostream>
#include <fstream>
#include <bitset>
#include <cstdlib>
using namespace std;
unsigned long L_orig,L,S,T,M,pos[102],dp[12000],ans;
bitset<12000> bridge,flag;
long memo(long i){
	if(i>=L)	return 0;
	if(flag[i])	return dp[i];
	flag[i]=1,dp[i]=INT_MAX;
	for(int j=S;j<=T;j++)
		if(dp[i]>memo(i+j))
			dp[i]=memo(i+j);
	dp[i]+=bridge[i];
	return dp[i];
}
inline int cmp(const void *a,const void *b){
	return *(long*)a-*(long*)b;
}
int main(){
	ifstream cin("river.in");
	cin >> L_orig >> S >> T >> M;
	if(S==T)
		for(int i=1,tmp;i<=M;i++){
			cin >> tmp;
			if(tmp%S==0)	ans++;
		}
	else{
		pos[M+1]=L_orig;
		for(int i=1,counter=0;i<=M;i++)	cin >> pos[i];
		qsort(pos,M+2, sizeof(pos[0]),cmp);
		for(int i=1;i<=M+1;i++){
			if(pos[i]-pos[i-1]-1<=100)	L+=pos[i]-pos[i-1];
			else	L+=100;
			bridge[L]=1;
		}
		ans=memo(0);
	}
	ofstream fout("river.out");
	fout << ans << endl;
	return 0;
}

[NOIP06] 2^k进制数

2^K进制数

【题目描述】

设r是个2^k 进制数,并满足以下条件:

(1)r至少是个2位的2^k进制数。

(2)作为2^k进制数,除最后一位外,r的每一位严格小于它右边相邻的那一位。

(3)将r转换为2进制数q后,则q的总位数不超过w。

在这里,正整数k(1≤k≤9)和w(k<w≤30000)是事先给定的。

问:满足上述条件的不同的r共有多少个?

我们再从另一角度作些解释:设S是长度为w 的01字符串(即字符串S由w个“0”或“1”组成),S对应于上述条件(3)中的q。将S从右起划分为若干个长度为k 的段,每段对应一位2k进制的数,如果S至少可分成2段,则S所对应的二进制数又可以转换为上述的2k 进制数r。

例:设k=3,w=7。则r是个八进制数(23=8)。由于w=7,长度为7的01字符串按3位一段分,可分为3段(即1,3,3,左边第一段只有一个二进制位),则满足条件的八进制数有:

2位数:高位为1:6个(即12,13,14,15,16,17),高位为2:5个,…,高位为6:1个(即67)。共6+5+…+1=21个。

3位数:高位只能是1,第2位为2:5个(即123,124,125,126,127),第2位为3:4个,…,第2位为6:1个(即167)。共5+4+…+1=15个。

所以,满足要求的r共有36个。

【输入】

输入只有1行,为两个正整数,用一个空格隔开:k W

【输出】

输出为1行,是一个正整数,为所求的计算结果,即满足条件的不同的r的个数(用十进制数表示),要求最高位不得为0,各数字之间不得插入数字以外的其他字符(例如空格、换行符、逗号等)。

(提示:作为结果的正整数可能很大,但不会超过200位)

【样例输入】

3 7

【样例输出】

36

【题解】

想起去年NOIP做到这题时,我还是巨巨巨巨巨巨巨菜,看了这题的标题就直接放弃了……现在看来这题也不是那么难的。

题目中“从另一角度作些解释”是很重要的,这几乎直接给我们提供了算法(往下看之前请仔细阅读题目中的相关部分)。令N为2^k进制数的最大允许位数,\(a_0\) 为最高位允许的最大值,则有:

$$N= \left\lceil \frac{W}{K} \right\rceil$$ $$a_0=2^{W \mod K}-1$$

那么问题其实就等同于:取N个数(对应于\(2^k\)进制数中的N位),第1个数num[1](对应于最高位)取值范围为[1,a0],其余第2~N个数num[i]取值范围都为[1,2^K)。现从其中第i(\( 1 \leq i \leq N-1\))个数num[i]开始取数,一直取到最后一个数num[N],要求对于任意的j>i满足num[j-1]<num[j]。问取数方案的总数。这就是很基础的组合数学问题了。

先不考虑第1个数,因为限制一个最大值会稍微麻烦一点。我们考虑一般情况。显然,如果取后i个数(\( 2 \leq i < N \)),可供选择的数有 \(2^k-1\) 个,则方案总数就为 \(C(i,2^k-1)\)。因此,从第2~N个数开始取的方案总数就为:$$\sum_{i=2}^{n-1}C_{2^k-1}^i$$

如果从第1个数开始取,则第1个数有1~\(a_0\)共\(a0\)个选择。如果选择a(\(1 \leq a \leq a_0\)),那么[1,\(2^K\))范围内剩下比a大的数就有\(2^k-a-1\)个,剩下N-1个数的可能选择方案就有\(C(2^k-i-1,n-1)\)个。那么从第1个数开始取的总方案数就为:$$\sum_{a=1}^{a_0}C_{2^k-a-1}^{n-1}$$

综上,要求的总方案数就为:$$ans=\sum_{a=1}^{a_0}C_{2^k-a-1}^{n-1}+\sum_{i=2}^{n-1}C_{2^k-1}^i$$

其中组合数\(C(N,K)\)可以利用下面递推式加上一点记忆化来在最短时间内计算得出 $$C_n^k=C_{n-1}^{k-1}+C_{n-1}^k$$

现在按照这个写应该会很简单了。这个如果用Pascal的int64据说能过7个点,用C++的unsigned long long则能过8个,性价比相当高了。但要想AC唯有高精。

源码:

#include <iostream>
#include <fstream>
#include <cmath>
#include <cassert>
#define SIZE 400
using namespace std;
class _int{
	int b[SIZE],l;
public:
	_int(){};
	_int(int n);
	int& operator()(const int&pos){return b[pos];}
	friend const _int operator+(const _int& b1,const _int& b2);
	friend ostream& operator<<(ostream& is,const _int& b);
};
_int::_int(int n){
	memset(b,0,sizeof(b));
	l=0;
	while(n!=0)
		b[l]=n%10,n/=10,l++;
}
const _int operator+(const _int& b1,const _int& b2){
	_int Ans=_int(0);
	Ans.l=b1.l;
	if(Ans.l<b2.l) Ans.l=b2.l;
	for(int i=0;i<Ans.l;i++) Ans.b[i]=b1.b[i]+b2.b[i];
	for(int i=0;i<Ans.l;i++){
		Ans.b[i+1]+=Ans.b[i]/10;
		Ans.b[i]%=10;
		if(Ans.b[Ans.l]!=0) Ans.l++;
	}
	return Ans;
}
ostream& operator<<(ostream& os,const _int& b){
	for(int i=b.l-1;i>=0;i–) os<<b.b[i];
	return os;
}
const unsigned pow2[10]={1,2,4,8,16,32,64,128,256,512};
unsigned K,W,N,a0;
_int ans,c[512][512];
bool flag[512][512];
_int C(int n,int k){
	if(n<k)	return _int(0);
	if(n==k||k==0)	return _int(1);
	assert(n<512);
	if(flag[n][k])	return c[n][k];
	flag[n][k]=1;
	return c[n][k]=C(n-1,k-1)+C(n-1,k);
}
int main(){
	ifstream cin("digital.in");
	cin >> K >> W;
	N=long(ceil(double(W)/K));
	a0=pow2[W%K]-1!=0?pow2[W%K]-1:pow2[K];
	for(int i=1;i<=a0&&pow2[K]-i-1>=N-1;i++)	ans=ans+C(pow2[K]-i-1,N-1);
	for(int i=2;i<=N-1&&pow2[K]-1>=i;i++)	ans=ans+C(pow2[K]-1,i);
	ofstream cout("digital.out");
	cout << ans << endl;
	return 0;
}

标签云

豆瓣