[POJ1771] Elevator Stopping Plan

Filed Under (新长征路上的代码) by 逆铭 on 2009-11-21 00:33

新浪的 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://gallery.tomtung.com/pictures/poj1771_2.png
以上两点很容易证明。由这两点可以得到一个很简单但足以解决问题的结论:

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

http://gallery.tomtung.com/pictures/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://gallery.tomtung.com/pictures/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();
}

Tags:,

[BHOJ10235] 窗口取数

Filed Under (新长征路上的代码) by 逆铭 on 2009-03-28 21:52

http://gallery.tomtung.com/pictures/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://gallery.tomtung.com/pictures/bhoj-10235_1.png

min = 6

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

http://gallery.tomtung.com/pictures/bhoj-10235_2.png

min = 2

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

http://gallery.tomtung.com/pictures/bhoj-10235_3.png

min = 2

其余如法炮制。

http://gallery.tomtung.com/pictures/bhoj-10235_4.png

min = 2

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

http://gallery.tomtung.com/pictures/bhoj-10235_5.gif

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

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

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

http://gallery.tomtung.com/pictures/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://gallery.tomtung.com/pictures/bhoj-10235_7.gif

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

http://gallery.tomtung.com/pictures/bhoj-10235_8.gif

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

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

http://gallery.tomtung.com/pictures/bhoj-10235_9.gif

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

http://gallery.tomtung.com/pictures/bhoj-10235_X.gif

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

以上就是整个解法。

三、关于栈

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

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

四、问题的扩展

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

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


Tags:, ,

[NOIP05] 篝火晚会

Filed Under (OI那一年) by 逆铭 on 2007-10-02 15:51

篝火晚会

(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里看到的算法。我自己没有实现过,有兴趣的试试吧。

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

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

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


Tags:

[NOIP05]过河

Filed Under (OI那一年) by 逆铭 on 2007-09-19 21:50

过河

(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;
}

Tags:,

[NOIP06] 2^k进制数

Filed Under (OI那一年) by 逆铭 on 2007-09-01 23:09

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进制数的最大允许位数,a0为最高位允许的最大值,则有:

N=(W/K)取上整,a0=2^(W mod K)-1

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

先不考虑第1个数,因为限制一个最大值会稍微麻烦一点。我们考虑一般情况。显然,如果取后i个数(2<=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~a0共a0个选择。如果选择a(1<=a<=a0),那么[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;
}

Tags:,

[VIJOS 1006]晴天小猪历险记之 Hill

Filed Under (OI那一年) by 逆铭 on 2007-08-25 19:34

晴天小猪历险记之 Hill

【背景 Background】

在很久很久以前,有一个动物村庄,那里是猪的乐园(^_^),村民们勤劳、勇敢、善良、团结……

不过有一天,最小的小小猪生病了,而这种病是极其罕见的,因此大家都没有储存这种药物。所以晴天小猪自告奋勇,要去采取这种药草。于是,晴天小猪的传奇故事便由此展开……

【描述 Description】

这一天,他来到了一座深山的山脚下,因为只有这座深山中的一位隐者才知道这种药草的所在。但是上山的路错综复杂,由于小小猪的病情,晴天小猪想找一条需时最少的路到达山顶,但现在它一头雾水,所以向你求助。

山用一个三角形表示,从山顶依次向下有1段、2段、3段等山路,每一段用一个数字T(1<=T<=100)表示,代表晴天小猪在这一段山路上需要爬的时间,每一次它都可以朝左、右、左上、右上四个方向走(**注意**:在任意一层的第一段也可以走到本层的最后一段或上一层的最后一段)。

晴天小猪从山的左下角出发,目的地为山顶,即隐者的小屋。

【输入格式 Input Format】

第一行有一个数n(2<=n<=1000),表示山的高度。

从第二行至第n+1行,第i+1行有i个数,每个数表示晴天小猪在这一段山路上需要爬的时间。

【输出格式 Output Format】

一个数,即晴天小猪所需要的最短时间。

【样例输入 Sample Input】

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

【样例输出 Sample Output】

10

【时间限制 Time Limitation】

各个测试点1s

【题解 Solution】

题目里说:在任意一层的第一段也可以走到本层的最后一段或上一层的最后一段。我们需要加上一句:反之亦然;这样就可以做了。

这是挺有意思的一题。对我这样的初学者来说,也是不算水题了。这题的原型来自经典的数字三角形,但是条件有所修改。题目简要描述如下:

给出一个N行数字三角形,其中第i行有i列。记i行j列的数字为(i,j),则求从(1,N)到(1,1)的一条路径,要求路径上的数字之和最小。移动规则为:到达(i,j)位置时只能向左、右、左上、右上四个方向移动。特殊地,三角形左右互通,即由(i,1)可移动到(i,i)或(i-1,i-1);(i,i)同理。

的确和经典的数字三角形很像吧,仅仅在移动规则上加了两条:1.允许同行的左右移动;2.左右互通。但是加上这小小的两个条件,题目就大不一样了。

下面是三种方法,但事实上前两种都是扯淡,是我在思考时得到的不可行方法。建议直接跳到第三条分割线之后。

注:各转移方程都没有把行两端情况单另写出来……

———————————第1条分割线凌空飞过—————————————

既然和经典问题这么像,那我们就先按照经典的方程来做吧。在经典方程上简单加上向左、向右两个决策,这样行不行呢?显然,现在出现了两种环,一种是向一个方向走再折回来,一种是一直走到行首或行末,然后绕一个大圈回来。这两种环的存在导致了后效性的产生。当然,这时候递归下降还是可行的(用一个bool数组记录哪些点已经到达),但是复杂度无疑是巨大的指数级。我就这么交了一次,结果一个点都没过……-_-

———————————第2条分割线凌空飞过—————————————

这样改动后真的失去无后效性了么?引起这一变化的原因就是同行内的移动,那我们就从同行内的移动考虑。在同行内移动时,一定是从左或从右直接移动到某一个点,然后向上移;绝对不会在一行里面走回头路,也不会赖在一行里不走的。这样,行间转移的无后效性就很明显了。若用dp(i,j)表示(i,j)到(1,1)的路径最小权和,sum(i,j,k)表示第i行中j列到k列的路径最小权和,很容易写出下面的方程:

dp(i,j)=min{ sum(i,j,k)+min{dp(i-1,j),dp(i-1,j-1)} } (1<=k<=i)

其中,sum(i,j,k)可以在O(1)时间内获得。DP的子问题数是O(N^2)的,转移是O(N)的,这样复杂度就是O(N^3)的,对N<=1000这个数据规模还是太大了。至于能得多少分,由于我没写,就不知道了。

———————————第3条分割线凌空飞过—————————————

事实上我们可以利用同行内的递推关系来获得O(N^2)的算法。我们把在第一条分割线下被否定的方程写出来:

dp(i,j)=min{ min{dp(i-1,j),dp(i-1,j)},dp(i,j-1),dp(i,j+1) }+num(i,j)

其中num(i,j)即为位置(i,j)上的数。即对于一个点(i,j),或者上移至前一行,或者在本行内向左或向右移动。由于不走回头路,若一个点(i,j)的dp值由左边的点(i,j-1)转移得到,则(i,j-1)也必由它自己的左边的点(i,j-2)或前一行转移得到而不必考虑向右。即:

若dp(i,j)=dp(i,j-1)+num(i,j)

则必有dp(i,j-1)=min{ dp(i,j-2),min{dp(i-1,j-2),dp(i-1,j-1)} }+num(i,j-1)(不必考虑转移到dp(i,j))

向右转移时也是同理。

现需找一个行内递推的起点。我们可取此行内min{dp(i-1,j),dp(i-1,j-1)}+num(i,j)最小的一点,因为它的dp值只能由上一行转移得到。以此为起点,向左向右分别推一圈,就得到了整行内的dp值。

就写这么多……感觉越写越乱了……还是看代码吧:

/*
  Name: Vijos P1006 晴天小猪历险记之Hill
  Author: 巨菜逆铭
  Date: 24-08-07 18:58
  Description: Dynamic Programming
*/

#include <iostream>
#include <fstream>
#include <cassert>
#define SYS sys\
tem
using namespace std;
unsigned long N,num[1001],dp[2][1002];
int main(){
	//The Amulet
	unsigned rp=unsigned(-1);

	//ifstream cin("input.txt");
	cin >> N >> dp[0][1];
	dp[0][0]=dp[0][2]=dp[0][1];
	bool flag=1;
	for(int i=2;i<=N;i++,flag=!flag){
		int m,m_num=INT_MAX;
		for(int j=1;j<=i;j++){
			cin >> num[j];
			dp[flag][j]=num[j]+min(dp[!flag][j],dp[!flag][j-1]);
			if(m_num>dp[flag][j])	m=j,m_num=dp[flag][j];
		}
		//walk rightward
		for(int j=m+1;j<=i;j++)
			if(dp[flag][j]>dp[flag][j-1]+num[j])
				dp[flag][j]=dp[flag][j-1]+num[j];
		if(dp[flag][1]>dp[flag][i]+num[1])
			dp[flag][1]=dp[flag][i]+num[1];
		for(int j=2;j<m;j++)
			if(dp[flag][j]>dp[flag][j-1]+num[j])
				dp[flag][j]=dp[flag][j-1]+num[j];
		//walk leftward
		for(int j=m-1;j>=1;j--)
			if(dp[flag][j]>dp[flag][j+1]+num[j])
				dp[flag][j]=dp[flag][j+1]+num[j];
		if(dp[flag][i]>dp[flag][1]+num[i])
			dp[flag][i]=dp[flag][1]+num[i];
		for(int j=i-1;j>m;j--)
			if(dp[flag][j]>dp[flag][j+1]+num[j])
				dp[flag][j]=dp[flag][j+1]+num[j];
		//special step for convenience
		dp[flag][0]=dp[flag][i];
		dp[flag][i+1]=dp[flag][1];
	}
	cout << dp[!flag][1] << endl;
	//SYS("pause");
	return 0;
}

Tags:,

[VIJOS 1014]旅行商简化版

Filed Under (OI那一年) by 逆铭 on 2007-08-15 21:25

旅行商简化版

【背景 Background】

欧几里德旅行商(Euclidean Traveling Salesman)问题也就是货郎担问题一直是困扰全世界数学家、计算机学家的著名问题。现有的算法都没有办法在确定型机器上在多项式时间内求出最优解,但是有办法在多项式时间内求出一个较优解。

为了简化问题,而且保证能在多项式时间内求出最优解,J.L.Bentley提出了一种叫做bitonic tour的哈密尔顿环游。它的要求是任意两点(a,b)之间的相互到达的代价dist(a,b)=dist(b,a)且任意两点之间可以相互到达,并且环游的路线只能是从最西端单向到最东端,再单项返回最西端,并且是一个哈密尔顿回路。

【描述 Description】

著名的NPC难题的简化版本

现在笛卡尔平面上有n(n<=1000)个点,每个点的坐标为(x,y)(-2^31<x,y<2^31,且为整数),任意两点之间相互到达的代价为这两点的欧几里德距离,现要你编程求出最短bitonic tour。

【输入格式 Input Format】

第一行一个整数n

接下来n行,每行两个整数x,y,表示某个点的坐标。

输入中保证没有重复的两点,保证最西端和最东端都只有一个点。

【输出格式 Output Format】

一行,即最短回路的长度,保留2位小数。

【样例输入 Sample Input】

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

【样例输出 Sample Output】

25.58

【时间限制 Time Limitation】

1s

【来源 Source】

《算法导论(第二版)》 15-1

【题解 Solution】

基本DP。

这题需要补充一个条件:任何两点的x坐标都不相等。

首先按照x坐标对所有点进行排序,并从西到东依次编号。这样我们就可以进行DP了。阶段可以按照从西到东的各个点来划分。

如图,我们对E点代表的阶段进行决策,有两种决策:E连B或E连D。这样,类似B、D的两个点就可以表示一种状态。

我们用dp[i][j]表示一种状态,表示在这两个点代表的状态之后,还能得到的最小长度。则有

dp[i][j]=dp[j][i]=min{dp[i][k]+dist[j][k],dp[k][j]+dist[i][k]}

(k=max(i,j)+1)

dp[0][0]就是所求的解。边界也很好处理。时间复杂度为O(N^2)。

源码:

#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <cmath>
using namespace std;
double dp[1000][1000],dist[1000][1000];
int N;
struct Dot{	long x,y;}dots[1000];
int cmp(const void *a, const void *b){
	return ((Dot*)a)->x-((Dot*)b)->x;
}
double memo(int a, int b){
	if(dp[a][b]>0)	return dp[a][b];
	if(a==N-1)	return dist[N-1][b];
	if(b==N-1)	return dist[N-1][a];
	int c=max(a,b)+1;
	return dp[b][a]=dp[a][b]=min(memo(a,c)+dist[b][c],memo(c,b)+dist[a][c]);
}
int main(){
	cin >> N;
	for(int i=0;i<N;i++)
		cin >> dots[i].x >> dots[i].y;
	qsort( dots, N, sizeof(dots[0]), cmp );
	for(int i=0;i<N;i++)
		for(int j=i+1;j<N;j++){
			double x=dots[i].x-dots[j].x,y=dots[i].y-dots[j].y;
			dist[i][j]=dist[j][i]=sqrt(x*x+y*y);
		}
	cout << fixed << setprecision(2) <<memo(0,0) << endl;
	return 0;
}

Tags:,

[NOI 05]聪聪与可可

Filed Under (OI那一年) by 逆铭 on 2007-07-20 21:07

聪聪与可可

主文件名:cchkk

时限:1s

【问题描述】

在一个魔法森林里,住着一只聪明的小猫聪聪和一只可爱的小老鼠可可。虽然灰姑娘非常喜欢她们俩,但是,聪聪终究是一只猫,而可可终究是一只老鼠,同样不变的是,聪聪成天想着要吃掉可可。

一天,聪聪意外得到了一台非常有用的机器,据说是叫GPS,对可可能准确的定位。有了这台机器,聪聪要吃可可就易如反掌了。于是,聪聪准备马上出发,去找可可。而可怜的可可还不知道大难即将临头,仍在森林里无忧无虑的玩耍。

小兔子乖乖听到这件事,马上向灰姑娘报告。灰姑娘决定尽快阻止聪聪,拯救可可,可她不知道还有没有足够的时间。

整个森林可以认为是一个无向图,图中有N个美丽的景点,景点从1至N编号。小动物们都只在景点休息、玩耍。在景点之间有一些路连接。

当聪聪得到GPS时,可可正在景点M(M≤N)处。以后的每个时间单位,可可都会选择去相邻的景点(可能有多个)中的一个或停留在原景点不动。而去这些地方所发生的概率是相等的。假设有P个景点与景点M相邻,它们分别是景点R、景点S,……景点Q,在时刻T可可处在景点M,则在(T+1)时刻,可可有1/(P+1)的可能在景点R,有1/(P+1)的可能在景点S,……,有1/(P+1)的可能在景点Q,还有1/(P+1)的可能停在景点M。

我们知道,聪聪是很聪明的,所以,当她在景点C时,她会选一个更靠近可可的景点,如果这样的景点有多个,她会选一个标号最小的景点。由于聪聪太想吃掉可可了,如果走完第一步以后仍然没吃到可可,她还可以在本段时间内再向可可走近一步。

在每个时间单位,假设聪聪先走,可可后走。在某一时刻,若聪聪和可可位于同一个景点,则可怜的可可就被吃掉了。

灰姑娘想知道,平均情况下,聪聪几步就可能吃到可可。而你需要帮助灰姑娘尽快的找到答案。

【输入格式】

从文件cchkk.in中读入数据。

数据的第1行为两个整数N和E,以空格分隔,分别表示森林中的景点数和连接相邻景点的路的条数。

第2行包含两个整数C和M,以空格分隔,分别表示初始时聪聪和可可所在的景点的编号。

接下来E行,每行两个整数,第i+2行的两个整数Ai和Bi表示景点Ai和景点Bi之间有一条路。

所有的路都是无向的,即:如果能从A走到B,就可以从B走到A。

输入保证任何两个景点之间不会有多于一条路直接相连,且聪聪和可可之间必有路直接或间接的相连。

【输出格式】

输出到文件cchkk.out中。

输出1个实数,四舍五入保留三位小数,表示平均多少个时间单位后聪聪会把可可吃掉。

【输入样例1】

 4 3 1 4 1 2 2 3 3 4

【输出样例1】

1.500

【输入样例2】

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

【输出样例2】

2.167

【数据范围】

对于所有的数据,1≤N,E≤1000。

对于50%的数据,1≤N≤50。

【题解】

NOI还有这么简单的题么……引用Ivan的话:是人就会做。汗,这话说的我做出来以后一点成就感都没有。

唯一一点要注意的就是别让题目阴了。“灰姑娘想知道,平均情况下,聪聪几步就可能吃到可可。”按着这个做就挂了。一定得看清楚样例分析(我这没贴),否则怎么知道这个“步”是指回合而不是真的指具体的步呢。

要拿50分,floyd预处理+递归下降。要拿满分,预处理+记忆化。我这里用的是,纯是为了复习。好像还快点(我也写了)。

源码:

#include <fstream>
#include <iomanip>
using namespace std;
int V,E,C,K;//节点数、边数、聪聪和可可的初始位置
short neighbor[1001][1001];//[i][0]:节点i连接的边数;[i][j]:节点i的第j个邻居
unsigned short dist[1001][1001];//[i][j]:i、j两点间的最短距离
double dp[1001][1001];
bool walk(short &c,short k){//让聪聪向可可走一步,并返回是否能吃到
    for(short j=1,c0=c,cc;j<=neighbor[c0][0];j++){
        cc=neighbor[c0][j];
        if(dist[c][k]>dist[cc][k]||(dist[c][k]==dist[cc][k]&&c>cc))
            c=cc;
    }
    return c==k;
}
double memo(short c,short k){
    short c0=c;
    if(dp[c][k]>0)    return dp[c][k];
    if(c==k)    return 0;
    if(walk(c,k)||walk(c,k))    return 1;//这句话诡异吧^^
    double ans=memo(c,k);
    for(short j=1;j<=neighbor[k][0];j++)
        ans+=memo(c,neighbor[k][j]);
    ans/=(neighbor[k][0]+1);
    return dp[c0][k]=ans+1;
}
int main(){
    ifstream fin("cchkk.in");
    ofstream fout("cchkk.out");
    fin >> V >> E >> C >> K;
    for(int i,j,k=0;k<E;k++){
        fin >> i >> j;
        neighbor[i][++neighbor[i][0]]=j;
        neighbor[j][++neighbor[j][0]]=i;
    }
    //用SPFA来预处理任意两点间的最短距离
    memset(dist,-1,sizeof(dist));
    for(int s=1;s<=V;s++){
        short queue[1001],head=0,tail=1;
        bool inqueue[1001]={0};
        queue[0]=s,dist[s][s]=0;
        while(head<tail){
            short u = queue[head++];
            inqueue[u]=false;
            if(head>1000)    head=0;
            for(short j=1,v;j<=neighbor[u][0];j++){
                v=neighbor[u][j];
                if(dist[s][v]>dist[s][u]+1){
                    dist[s][v]=dist[s][u]+1;
                    if(!inqueue[v]){
                        queue[tail++]=v,inqueue[v]=true;
                        if(tail>1000)    tail=0;
                    }
                }
            }
        }
    }
    fout << fixed << setprecision(3) << memo(C,K) << endl;
    return 0;
}

Tags:, , ,

[NOI06]生日快乐(using SBT)

Filed Under (OI那一年) by 逆铭 on 2007-07-17 11:58

第二十三届全国信息学奥林匹克竞赛
NOI 2006
第一试

题目名称 生日快乐
可执行文件名 happybirthday
输入文件名 N/A
输出文件名 N/A
时限 4 秒
测试点数目 10
每测试点分值 10
是否有部分分
题目类型 交互

生日快乐

【任务描述】

今天是栋栋的生日,他邀请了N 个好友参加Party 。朋友们都知道,栋栋最喜欢吃果冻。因此, 每个朋友带来的生日礼物全是一包果冻.。

在每个朋友送他一包果冻的同时,栋栋还要这个朋友送他一个幸运号码L(1 ≤ L ≤ N)。然后栋栋会先把这包果冻放在一旁,并且把之前的所有果冻包按照果冻的数量从小到大排序(如果果冻数量相等,先后顺序任意)。接着,栋栋再把当前这包果冻插入到有序的果冻包队列中,使得这个队列仍然有序(如果存在其他的果冻包与该果冻包数量相等,则把该果冻包放在它们的前面)。完成这个操作后,栋栋就会进行如下操作:

“如果这个朋友是男生,栋栋会从他送的包的后一个包开始向后数L 个(该朋友的幸运号码),从那个包里取出一个果冻,吃掉。

“如果这个朋友是女生,栋栋会从她送的包的前一个包开始向前数L 个(该朋友的幸运号码),从那个包里取出一个果冻,吃掉。

栋栋实在是太粗心了,以至于他收完所有的礼物后,都不知道吃过哪些朋友的果冻,现在,他希望你帮他一下,当他每吃一个果冻后马上告诉他可能吃的是谁送的(由于排序不是确定的,所以栋栋只要你给他一种可能的答案就行了)。

这是一个交互式的题目,你必须调用库函数来完成所有操作而不能访问任何文件。

【数据规模和约定】

对于所有的数据,我们保证:1 ≤ n ≤ 500000,0 ≤ count ≤ 10^8 ,1 ≤ luckynumber ≤ n.在测试时,你的数据也应该满足我们的数据范围,否则有可能运行异常。

【题解】

平衡树的的典型应用。我这里对SBT(Size Balanced Tree)进行2处改写来做这题:首先是在Insert过程中返回插入节点的秩,这是一个顺便的改动,非常简单;再就是在Delete和Select的基础上改写出了SBT_Select_Delete,即选择指定秩的节点删除,并返回此节点。这个也不难,看看我的代码就知道了。只是注意,既然删除的节点要回收利用,那么从树中删除后就需要处理清楚size和ch[]域。

BTW,题目中给的count范围包括0,这是不负责任的。仔细想想就发现一旦出现这样的节点你就无法确定该如何处理了。当然了,评测时并没有给出这样的数据,否则鬼知道怎么处理呢。

时空开销:

代码:

#include "happybirthday_lib_c.cpp"
struct SBTNode{
	SBTNode *ch[2];
	long size, key, id;
	SBTNode(long _key, long _id, long _size);
}NIL=SBTNode(0,0,0);
SBTNode::SBTNode(long _key, long _id, long _size=1){
	key=_key,id=_id,size=_size;
	ch[0]=ch[1]=&NIL;
}
typedef SBTNode* SBTree;
void SBT_Rotate(SBTree &x, bool flag){
	SBTNode *y = x->ch[!flag];
	x->ch[!flag]=y->ch[flag],y->ch[flag]=x;
	y->size=x->size;
	x->size=x->ch[0]->size+x->ch[1]->size+1;
	x=y;
}
void SBT_Maintain(SBTree &T,bool flag){
	if(T->ch[flag]->ch[flag]->size>T->ch[!flag]->size)
		SBT_Rotate(T,!flag);
	else if(T->ch[flag]->ch[!flag]->size>T->ch[!flag]->size)
		SBT_Rotate(T->ch[flag],flag),SBT_Rotate(T,!flag);
	else return;
	SBT_Maintain(T->ch[0],0),SBT_Maintain(T->ch[1],1);
	SBT_Maintain(T,0),SBT_Maintain(T,1);
}
long SBT_Insert(SBTree &T, SBTNode *toAdd){
	if(T==&NIL){
		T=toAdd;
		return 1;
	}else{
		T->size++;
		bool flag=toAdd->key>T->key;
		long r=SBT_Insert(T->ch[flag],toAdd)+(flag?T->ch[0]->size+1:0);
		SBT_Maintain(T,flag);
		return r;
	}
}
SBTNode *SBT_Select_Delete(SBTree &T,long i){
	if(T==&NIL)	return &NIL;
	T->size--;
	unsigned long r = T->ch[0]->size + 1;
	if(i==r){
		SBTNode *toDel = NULL;
		if(T->ch[0]==&NIL||T->ch[1]==&NIL){
			toDel=T;
			T=T->ch[T->ch[1]!=&NIL];
		}else{
			toDel = SBT_Select_Delete(T->ch[1],1);
			swap(T->key,toDel->key),swap(T->id,toDel->id);
		}
		toDel->ch[0]=toDel->ch[1]=&NIL;
		toDel->size=1;
		return toDel;
	}
	else return SBT_Select_Delete(T->ch[i>r],i>r?i-r:i);
}
int main(){
	long c,l,r,id=1; bool isboy;
	SBTNode *toEat; SBTree T=&NIL;
	for(init();getpresent(c,l,isboy);id++){
		SBTNode *toAdd = new SBTNode(c,id);
		r = SBT_Insert(T,toAdd);
		if(isboy)	r+=l;
		else r-=l;
		if(r<=0||r>T->size) tell(-1);
		else{
			toEat = SBT_Select_Delete(T,r);
			tell(toEat->id);
			if(--toEat->key>0)	SBT_Insert(T,toEat);
			else delete toEat;
		}
	}
	return 0;
}

P.S.有一件事情很郁闷,那就是……这题用不着平衡树-_-|||

因为数据都是random的,普通BST表现反倒会更好。。。(但是比赛时谁知道呢)


Tags:, ,

[CEOI99][POJ1733][URAL1003][VIJOS1112]Parity Game

Filed Under (OI那一年) by 逆铭 on 2007-07-12 16:12

Parity Game

Time Limit:1000MS

Memory Limit:65536K

Description

Now and then you play the following game with your friend. Your friend writes down a sequence consisting of zeroes and ones. You choose a continuous subsequence (for example the subsequence from the third to the fifth digit inclusively) and ask him, whether this subsequence contains even or odd number of ones. Your friend answers your question and you can ask him about another subsequence and so on. Your task is to guess the entire sequence of numbers.

You suspect some of your friend’s answers may not be correct and you want to convict him of falsehood. Thus you have decided to write a program to help you in this matter. The program will receive a series of your questions together with the answers you have received from your friend. The aim of this program is to find the first answer which is provably wrong, i.e. that there exists a sequence satisfying answers to all the previous questions, but no such sequence satisfies this answer.

Input

The first line of input contains one number, which is the length of the sequence of zeroes and ones. This length is less or equal to 1000000000. In the second line, there is one positive integer which is the number of questions asked and answers to them. The number of questions and answers is less or equal to 5000. The remaining lines specify questions and answers. Each line contains one question and the answer to this question: two integers (the position of the first and last digit in the chosen subsequence) and one word which is either ‘even’ or ‘odd’ (the answer, i.e. the parity of the number of ones in the chosen subsequence, where ‘even’ means an even number of ones and ‘odd’ means an odd number).

Output

There is only one line in output containing one integer X. Number X says that there exists a sequence of zeroes and ones satisfying first X parity conditions, but there exists none satisfying X+1 conditions. If there exists a sequence of zeroes and ones satisfying all the given conditions, then number X should be the number of all the questions asked.

Example

Example 1:

PARITY.IN

10
5
1 2 even
3 4 odd
5 6 even
1 6 even
7 10 odd

PARITY.OUT

3

Example 2:

PARITY.IN

10
5
1 2 even
1 4 even
2 4 odd
1 10 even
3 10 even

PARITY.OUT

5

Solution

挖个坑先……要是比赛中的话我怎么可能想到这题是并查集呢!

终于搞定。

首先有一步关键转化要做。我们同时处理一个范围内数字1的个数的奇偶比较麻烦。如果a到b之间有n个1,那么可以看成0到b之间1的个数减去0到a-1之间1的个数为n。我们假想知道了所有1的位置,那么0到任意i之间1的个数就是确定的。也就是说,通过这种转化,数据范围内每个数都对应了一个确定的数,它表示0到此数之间数字1的个数。

而我们事实上并不知道这些数确切是什么。我们知道的是一些数对之间奇偶性的关系。而这种关系是可以传递转移的。我们需要判断在给出关系的过程中是否出现矛盾。

于是就可以用并查集来解决这个问题了。它与我前面做过的食物链相比,本质上是一样的,但是处理起来还简单。你可以参照那个题的题解来理解这个题。

除了这步转化以外,对我而言处理起来比较麻烦的就是离散化了。这题串最大长度为10亿,而仅仅给出最多5千条询问,明显不离散化是不行了。我用的Hash。Hash和并查集的联合我从前是没搞过的。

P.S.这题其实是个骗分的好题(ACMer们别打我。。。)。不离散化最少70分。如果内存放宽一点就有80分。即使奇偶搞反了都有50分……总之怎么着都能得分的。CEOI的标程好像不是用并查集做的,但是要慢一些。就不管它了。

时空开销:

URAL的数据ms有问题,就再不管了。

源码:

#include <fstream>
#include <string>
using namespace std;
const long P=29989;
const string odd="odd";
long N,K;
struct elem{
	long key,rank;
	bool p_r;//false means same
	elem *next,*p;
	elem(long _key){key=_key,rank=0,p=this,p_r=false,next=NULL;}
}*set[P];
elem* Hash_Search(long key){
	elem* p=set[key%P];
	while(p&&p->key!=key)	p=p->next;
	return p;
}
void Hash_Insert(elem* toAdd){
	long h = toAdd->key%P;
	toAdd->next=set[h];
	set[h]=toAdd;
}
elem *Set_Find(elem* A, bool &ch_r){
	if(A->p!=A){
		A->p=Set_Find(A->p,A->p_r);
		ch_r=(ch_r!=A->p_r);
	}
	return A->p;
}
bool istrue(long a,long b, bool flag){
	bool tmp=0;
	elem *A=Hash_Search(a),*B=Hash_Search(b);
	if(!A)	A = new elem(a),Hash_Insert(A);
	if(!B)	B = new elem(b),Hash_Insert(B);
	if(Set_Find(A,tmp)!=Set_Find(B,tmp)) return true;
	if(!flag) return A->p_r==B->p_r;
	else return A->p_r!=B->p_r;
}
void Set_Union(long a,long b, bool flag){
	elem *A=Hash_Search(a),*B=Hash_Search(b);
	if(!A)	A = new elem(a),Hash_Insert(A);
	else A=Set_Find(A,flag);
	if(!B)	B = new elem(b),Hash_Insert(B);
	else B=Set_Find(B,flag);
	if(A==B)	return;
	if(A->rank<B->rank)	A->p=B,A->p_r=flag;
	else{
		B->p=A,B->p_r=flag;
		if(A->rank==B->rank)	A->rank++;
	}
}
int main(){
	ifstream fin("PARITY.IN");
	ofstream fout("PARITY.OUT");
	fin >> N >> K;
	unsigned long a,b,ans=0;
	string parity;
	for(ans=1;ans<=K;ans++){
		fin >> a >> b >> parity;
		bool flag = parity==odd;//false means same
		if(istrue(a-1,b,flag))	Set_Union(a-1,b,flag);
		else break;
	}
	fout << --ans << endl;
	fin.close(),fout.close();
	return 0;
}

Tags:, ,
Get Adobe Flash playerPlugin by wpburn.com wordpress themes

Search:

Codes, Notes & Scribbles Rss