[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:,

[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:,

又一件郁闷事

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

今天在随手翻《奥赛经典》的时候,竟然看到了NOI那道我得分很低的DP题,几乎原原本本地出现在了233页上,而且包括了详解……

看来我的曾的确确错失了很多机会。但愿这样的事情不再发生。


Tags:,

[VIJOS 1037]搭建双塔

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

搭建双塔

【描述 Description】

2001年9月11日,一场突发的灾难将纽约世界贸易中心大厦夷为平地,Mr. F曾亲眼目睹了这次灾难。为了纪念“9?11”事件,Mr. F决定自己用水晶来搭建一座双塔。

Mr. F有N块水晶,每块水晶有一个高度,他想用这N块水晶搭建两座有同样高度的塔,使他们成为一座双塔,Mr. F可以从这N块水晶中任取M(1≤M≤N)块来搭建。但是他不知道能否使两座塔有同样的高度,也不知道如果能搭建成一座双塔,这座双塔的最大高度是多少。所以他来请你帮忙。

给定水晶的数量N(1≤N≤100)和每块水晶的高度Hi(N块水晶高度的总和不超过2000),你的任务是判断Mr. F能否用这些水晶搭建成一座双塔(两座塔有同样的高度),如果能,则输出所能搭建的双塔的最大高度,否则输出“Impossible”。

【输入格式 Input Format】

输入的第一行为一个数N,表示水晶的数量。第二行为N个数,第i个数表示第i个水晶的高度。

【输出格式 Output Format】

输出仅包含一行,如果能搭成一座双塔,则输出双塔的最大高度,否则输出一个字符串“Impossible”。

【样例输入 Sample Input】

5

1 3 4 5 2

【样例输出 Sample Output】

7

【题解 Solution】

应该说是很简单的dp,但是我没有独立完成……说明我果然是巨菜啊……

状态表示是我没见过的-_-。。。看了下别人的状态表示,然后就很容易地写出了方程。

dp(i,j)表示用前i块水晶堆2个塔,两塔间高度差为j时,较低塔的最大高度。

dp(i,j)=max{ dp(i-1,j+h[i]),dp(i-1,j-h[i])+h[i],dp(i-1,h[i]-j)+j,dp(i-1,j) }

注意处理边界……我还因为边界问题WA了一次,sigh……

源码:

#include <fstream>
#include <iostream>
#include <climits>
#include <cassert>
using namespace std;
int N,H[101],dp[101][2001];
bool flag[101][2001];
int memo(int i,int j){
	if(j<0)	return INT_MIN;
	if(i==0&&j==0)	return 0;
	if(i==0)	return INT_MIN;
	assert(i<=N&&j<=2000);
	if(flag[i][j])	return dp[i][j];
	int ans=INT_MIN;
	if(ans<memo(i-1,j))	ans=memo(i-1,j);//do not put
	if(ans<memo(i-1,j+H[i]))	ans=memo(i-1,j+H[i]);//put on the higher
	if(ans<memo(i-1,j-H[i])+H[i])	ans=memo(i-1,j-H[i])+H[i];//put on the lower
	if(ans<memo(i-1,H[i]-j)+j)	ans=memo(i-1,H[i]-j)+j;//put on the lower,and it becomes the higher
	flag[i][j]=true;
	return dp[i][j]=ans;
}
int main(){
	//The Amulet
	unsigned rp=unsigned(-1);

	//ifstream cin("input.txt");
	cin >> N;
	for(int i=1;i<=N;i++)	cin >> H[i];
	int ans = memo(N,0);
	if(ans<=0)	cout << "Impossible" << endl;
	else 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:,

[01ACM NEEuropean][URAL1183]Brackets sequence

Filed Under (OI那一年) by 逆铭 on 2007-06-01 16:25

Brackets sequence

Time Limit: 1.0 second

Memory Limit: 16 MB

Let us define a regular brackets sequence in the following way:

  1. Empty sequence is a regular sequence.
  2. If S is a regular sequence, then (S) and [S] are both regular sequences.
  3. If A and B are regular sequences, then AB is a regular sequence.

For example, all of the following sequences of characters are regular brackets sequences:

(), [], (()), ([]), ()[], ()[()]

And all of the following character sequences are not:

(, [, ), )(, ([)], ([(]

Some sequence of characters ‘(‘, ‘)’, ‘[', and ']‘ is given. You are to find the shortest possible regular brackets sequence, that contains the given character sequence as a subsequence. Here, a string a1a2…an is called a subsequence of the string b1b2…bm, if there exist such indices 1 ≤ i1 < i2 < … < in ≤ m, that aj=bij for all 1 ≤ j ≤ n.

Input

The input file contains at most 100 brackets (characters ‘(‘, ‘)’, ‘[' and ']‘) that are situated on a single line without any other characters among them.

Output

Write to the output file a single line that contains some regular brackets sequence that has the minimal possible length and contains the given sequence as a subsequence.

Sample

Input Ouput
([(] ()[()]

Problem Author: Andrew Stankevich
Problem Source: 2001-2002 ACM Northeastern European Regional Programming Contest

Solution

终于过了这题……WA了两次。第一次是判断括号匹配的栈写错了,WA#9;第二次是没有考虑输入文件是空串的情况,WA#13并持续一周 -_-|||

而且这次也写垃圾了,长达80多行。好像原来的ural的内存限制是1000K?这样说我开一个100*100的string数组真是件奢侈的事情呢……

#include <iostream>
#include <fstream>
#include <string>
#include <cassert>
using namespace std;
ifstream fin("bracket.in");
ofstream fout("bracket.out");
string S;
inline bool isregular(int i,int j)	//判断原串中的子串S[i...j]是否规则
{
	if(j<i)	return true;
	else if(j==i)	return false;
	char stack[100];	int p=0;
	for(int k=i;k<=j;k++)
	{
		if(p==0)
		{
			if(S[k]=='('||S[k]=='[')	stack[p++]=S[k];
			else return false;
		}
		else{
			if(( stack[p-1]=='('&&S[k]==')' )||( stack[p-1]=='['&&S[k]==']' ))	p--;
			else if(S[k]==')'||S[k]==']')	return false;
			else	stack[p++]=S[k];
		}
	}
	return (p==0);
}
string s[100][100];	//[i][j]:按照最少原则添加括号规则化S[i...j]后得到的串
string memo(int i, int j)	//记忆化搜索
{
	if(s[i][j]!="")	return s[i][j];
	if(isregular(i,j))	//若S[i...j]本身就已经是规则的
	{
		s[i][j].assign(S,i,j-i+1);
		return s[i][j];
	}
	if(i==j)	//若当前串仅由一个字符组成
	{
		if(S[i]=='('||S[i]==')')	s[i][j]="()";
		else if(S[i]=='['||S[i]==']')	s[i][j]="[]";
		else assert(0);
		return s[i][j];
	}
	string ans,tmp,tmp2;
	unsigned size=UINT_MAX;
	if(( S[i]=='('&&S[j]==')' )||( S[i]=='['&&S[j]==']' ))	//若S[i...j]首尾配对,则只需使S[i+1...j-1]规则就得到一个解
	{
		ans=S[i]+memo(i+1,j-1)+S[j];
		size=ans.size();
	}
	else if(( S[i]=='('&&S[j]!=')' )||( S[i]=='['&&S[j]!=']' ))	//S[i...j]首尾不配对,与以上类似
	{
		ans=S[i]+memo(i+1,j);
		if(S[i]=='(')	ans=ans+')';
		else if(S[i]=='[')	ans=ans+']';
		else assert(0);
		size=ans.size();
	}
	else if(( S[i]!='('&&S[j]==')' )||( S[i]!='['&&S[j]==']' ))
	{
		ans=memo(i,j-1)+S[j];
		if(S[j]==')')	ans='('+ans;
		else if(S[j]==']')	ans='['+ans;
		else assert(0);
		size=ans.size();
	}
	for(int k=i;k<j;k++)	//规则化S[i...k]和S[k+1...j]后合并得到的串
		if(size>memo(i,k).size()+memo(k+1,j).size())
		{
			ans=memo(i,k)+memo(k+1,j);
			size=memo(i,k).size()+memo(k+1,j).size();
		}
	return (s[i][j]=ans);
}
int main()
{
	fin >> S;
	if(S.size()==0)
	{
		fout << endl;
		return 0;
	}
	fout << memo(0,S.size()-1) << endl;
	return 0;
}

Tags:,

[IOI2000国家队原创题] 艺术馆的火灾

Filed Under (OI那一年) by 逆铭 on 2007-05-30 21:26

艺术馆的火灾

Fire of the Art Gallery

IOI2000 National Training Team Originals

Author: 张辰

背景描述

这幢古老的建筑是一个艺术馆,它珍藏着上百件绘画、雕塑以及其他艺术品,就连建筑本身也是一件艺术。但是,岁月并不在乎它的精致与美丽,时光在渐渐剥夺着这幢木屋的生命。终于,在一个月色昏暗的夜晚,它着火了。

艺术馆是一幢两层的小楼,每一层有N个房间,每个房间中收藏的艺术品的价值都可以用一个正整数来表示。下面是一个N=4的例子。

在这个例子中,二层楼的第四个房间中艺术品的价值最大,为60。而一层楼的第四个房间中艺术品的价值仅为20。

在消防队员火速赶到的时候,火势已经蔓延了整个建筑,消防队员们观察每个房间中的火势,将它们分别用一个正整数来表示。在上面的例子中,各房间中的火势可能如下。

你可以看到,二层楼的第四个房间中火势最强,为70。而一层楼的第三个房间中火势较弱,为20。

由于火情紧急,消防队员们准备使用一种新型的灭火器。这种灭火器只能发射K次,每次发射将覆盖一个矩形的区域(矩形的高度可以是1也可以是2)。它的威力巨大,所到之处不但火焰会被扑灭,就连同在一处的艺术品也难以幸免。因此,如何善用这种灭火器成了最大的问题。

一个例子:如果灭火器的一次发射覆盖了下图阴影所示的2×2矩形区域,那么这四个房间的火势和艺术品价值都将成为0。

任务说明

给出艺术馆每层的房间数N和灭火器的发射次数K,以及每个房间中艺术品的价值和火势,你需要决定灭火器每次应该怎样发射(也可以不发射),才能将这次火灾的损失降到最低限度。这个损失用你所摧毁的艺术品的总价值,加上剩余的火势总值(这些火焰将需要消防队员们亲身去扑灭)来衡量。

输入数据

输入文件的第一行有两个整数N(1 <= N <= 100)、K(1 <= K <= 10),分别表示艺术馆中每层的房间数和灭火器的发射次数。

接下来的两行每行有N个整数,其中第4-i行的第j个整数Vi,j表示的是第i层第j个房间中艺术品的价值(1 <= i <= 2,1 <= j <= N,1 <= Vi,j <= 10000)。

再接下来的两行每行也有N个整数,其中第6-i行的第j个整数Fi,j表示的是第i层第j个房间中的火势(1 <= i <= 2,1 <= j <= N,1 <= Fi,j <= 10000)。

输出数据

你的输出文件应该有K行,每行有四个整数L1、R1、L2、R2,表示你的灭火器的一次行动。如果灭火器这次不发射,那么这四个整数都为0;否则这次发射所覆盖的矩形区域的左下角是第L1层的第R1个房间,右上角是第L2层的第R2个房间。

注意:你的每次发射所覆盖的矩形区域必须位于小楼之内,并且矩形的面积至少为0。即1 <= L1 <= L2 <= 2,1 <= R1 <= R2 <= N。

输入输出样例

INPUT.TXT OUTPUT.TXT 样例图示
4 2

40 50 30 60

30 30 40 20

50 40 50 70

40 50 20 30

1 1 1 2

2 3 2 4

摧毁艺术品:30+60+30+30=150

剩余火势:50+40+20+30=140

最小损失:150+140=290

评分标准

每个测试点都有一定的时间限制。你的程序只有在规定的时限内输出解,并且按照你的方案来发射灭火器可以将损失降到最小,你才会得到相应测试点的分值;否则这个测试点不得分。

注意:本题的解不一定是唯一的。即对于一个测试数据,可能有多种发射灭火器的方案都可以使损失达到最小。在这种情况下,你只要输出最优方案中的任意一种即可。

解题报告

自己独立做出来的好有成就感啊……^_^

做这题的过程还相当传奇。话说那天生物课没拿书,直接被老师赶出去到物理办公室门口站着。岂知那里却安静、光线明亮,清风吹拂,透过窗口可以看见墙上大片翠绿的藤蔓植物在风中生起层层波浪,让人心旷神怡,真是个绝佳的自习的所在。当时手里也没书,没事干,就想起了这道本来一看来源就已经准备直接看题解的题目,和老师要了纸笔,想啊想啊就把这题搞出来了。-_-|||

不扯了,下面说说这题的做法(和官方解法不太一样~)。

这题的模型可以看成是一个2*N的矩形表格中每格存有有序数对(V,F),现在要从表格中选出一些格子,要求:

1.(选出格子中的V)+(未选中格子中的F)值最小;

2.这些格子能组成的最少完整矩形个数不大于定值K。

在第2个要求中我们强调“最少”和“完整”的矩形,这样才能得到最优解。因此,我们可以设法求出一种满足条件的格子选择,然后再按照最少原则用贪心算法划分矩形,且规定划分得到的矩形互不相交(因为相交时得到的解一定不优于此)。以样例数据为例,先选出(1,1),(1,2),(2,3),(2,4)四个格子,然后再把它们组合成(1,1,1,2)和(2,3,2,4)两个完整矩形。这已经是这四个格子所能组成的最少矩形数目了。(下文中无特殊说明,提到的“矩形”都指满足“最少”和“完整”要求的。)

由于表格的高已经确定为2这个很小的值,我们可以根据每一列的损失数来划分阶段。我们用(i,j,k)来表示一个状态。其中i表示当前状态为第i列,用于阶段划分;k表示此次决策前已经得到的矩形个数,用以在状态转移时限制矩形的个数;j表示要决策列的前一列中的灭火器使用情况,用于在状态转移时更新矩形的数目(后面我们可以看到当前列的决策是否导致新矩形的产生完全取决于前一列的灭火器使用情况)。

选择不同的递推方向会导致不同的状态定义和转移方程。这里我们选择顺推,因为每次决策都会唯一对应一个状态后继,这样转移方程写起来会简单许多。

我们的状态定义为:dp(i,j,k)表示前i列已经有k个矩形、第i列的状态为j之后还能获得的最小损失。转移方程为:dp(i,j,k)=min{dp(i+1,jj,kk)+loss(i+1,jj)}。其中jj表示决策第i+1行灭火器的使用情况。kk为因此得到的矩形数目,即若决策jj导致了矩形个数的增加,则kk=k+1,否则kk=k。loss(i,j)为第i列灭火器使用情况为j时的损失。

每一列的灭火器使用情况(j的取值)有4种:0.楼上楼下都不使用灭火器;1.楼上使用;2.楼下使用;3.上下都用。如果我们画两列格子看看,就会发现矩形数目不增加当且仅当jj=0或j==jj。这样我们就可以通过转移方程求出解dp(0,0,0)。

——但是等等!


这就是我第一次写时出现的疏忽(这让我第一次WA了4个点并调试了整整一个晚上)。我们没有注意到当j=3,jj=1(或2,这里以1为例)时可能有以下两种情况:

其中前一种如我们所想,jj决策导致了矩形数目的增加,但是后一种情况却不是这样。虽然j=3,jj=1,满足矩形数目增加的条件,但是矩形数目实际并没有增加。这是由于更早些的决策已经使第i行的上下两层分属两个矩形。因此我们需要为上下都使用的决策留出两个j值以示区别。这部分细节请看代码。

这样得到的状态定义和转移方程明显满足最有子结构和无后效性。我们可以据此求出最小损失。

这道题并不要求输出最小损失,而是要求输出灭火器每次使用的覆盖范围。这我们就需要构造最优解。我的方法很朴素。在dp过程中维护一个数组build[i][j][k],其值为状态(i,j,k)做出的最优决策jj。dp结束后,我们就可以递归地在一个bool数组上标记出那些使用了灭火器的格子。最后用贪心(细节见代码)把它们组合成矩形输出,这样问题就解决了。

这个算法思路比较简单,编程复杂度与官方解相当。至于时空复杂度,由于我没有系统学习过相关理论,不好乱说,ms是空间复杂度比官方解高,但是时间复杂度比官方解低?

#include <iostream>
#include <fstream>
#include <cassert>
using namespace std;
ifstream fin("Input.txt");
ofstream fout("Output.txt");
int room_n,shoot_k,v[3][101],f[3][101];

/*dp(memoization)部分*/
int dp[101][5][11];//[i][j][k]:前i列已经最少有k个完整矩形、第i列的状态为j之后还能获得的最小损失
bool checked[101][5][11];
int memo(int i,int j, int k);
int loss(int i, int j);	//memo调用的子过程,返回第i行决策为j时的净损失

/*最优解的构造部分*/
int build[101][5][11];	//记录(i,j,k)状态下要得到最优解,i+1列的状态
bool room[3][101];
void paint(int i, int j, int k);//在room中标出状态(i,j,k)后的灭火器使用情况

/*最优解的输出部分*/
int counter;//记录发射次数
void output(int i);	//输出矩形

int main()
{
	/*输入部分*/
	fin >> room_n >> shoot_k;
	for(int i=2;i>=1;i--)
		for(int j=1;j<=room_n;j++)
			fin >> v[i][j];
	for(int i=2;i>=1;i--)
		for(int j=1;j<=room_n;j++)
			fin >> f[i][j];
	/*计算部分*/
	unsigned min_loss=memo(0,0,0);
	paint(0,0,0);

	/*输出部分*/
	for(int i=1;i<=room_n;i++)	output(i);
	for(int j=shoot_k-counter;j>=1;j--)
		fout << "0 0 0 0\n";
	return 0;
}

//
int memo(int i,int j, int k)
{
	if(checked[i][j][k])	return dp[i][j][k];
	if(i==room_n)	return 0;
	dp[i][j][k]=INT_MAX;
	for(int jj=0,kk,ans;jj<=3;jj++)	//枚举第i+1行状态
	{
		if(jj==3&&(j==4||j==1||j==2))	jj++;	//上下都用时的状态修正
		kk=k+!(jj==0||jj==j||j==4);
		if(kk<=shoot_k)
			ans=memo(i+1,jj,kk)+loss(i+1,jj);
		else ans=INT_MAX;
		if(dp[i][j][k]>ans)
		{
			dp[i][j][k]=ans;
			build[i][j][k]=jj;
		}
	}
	assert(dp[i][j][k]<INT_MAX);
	checked[i][j][k]=true;
	return dp[i][j][k];
}
int loss(int i, int j)
{
/*状态j的定义:
	0:上层下层都不使用灭火器;
	1:上使用;2:下使用;3,4:上下都用
	其中3表示与此列相连的使用灭火器的房间最少组成1个完整矩形
	4表示与此列相连的使用灭火器的房间已经最少组成2个完整矩形*/
	switch(j)
	{
		case 0:
			return (f[1][i]+f[2][i]);	//上下都不用
		case 1:
			return(v[2][i]+f[1][i]);	//上用
		case 2:
			return(v[1][i]+f[2][i]);	//下用
		case 3:	//上下都用
		case 4:
			return(v[1][i]+v[2][i]);
	}
	assert(0);
}
//
void paint(int i, int j, int k)
{
	if(i==room_n)	return;
	assert(checked[i][j][k]);
	int jj=build[i][j][k];
	int kk=k+!(jj==0||jj==j||j==4);
	switch(jj)
	{
		case 0:
			break;	//上下都不用
		case 1:
			room[2][i+1]=1; break;	//上用
		case 2:
			room[1][i+1]=1; break;	//下用
		case 3:
		case 4:
			room[1][i+1]=room[2][i+1]=true; break;	//上下都用
		default:
			assert(0);
	}
	paint(i+1,jj,kk);
}
//
void output(int i)
{
	counter++;
	int ii;
	if(room[2][i]&&!room[1][i])	//上用
	{
		fout << 2 << ' ' << i << ' ';
		for(ii=i;ii<=room_n&&room[2][ii];ii++)	room[2][ii]=0;
		fout << 2 << ' ' << ii-1 << endl;
	}
	else if(room[1][i]&&!room[2][i])	//下用
	{
		fout << 1 << ' ' << i << ' ';
		for(ii=i;ii<=room_n&&room[1][ii];ii++)	room[1][ii]=0;
		fout << 1 << ' ' << ii-1 << endl;
	}
	else if(room[1][i]&&room[2][i])	//上下都用
	{
		fout << 1 << ' ' << i << ' ';
		for(ii=i;ii<=room_n&&room[1][ii]&&room[2][ii];ii++)	room[1][ii]=room[2][ii]=0;
		fout << 2 << ' ' << ii-1 << endl;
	}
	else counter--;	//上下都不用
}
//

Tags:,

[NOI97]积木游戏(Game)

Filed Under (OI那一年) by 逆铭 on 2007-05-27 18:23

积木游戏(NOI’97)

SERCOI 最近设计了一种积木游戏。每个游戏者有N块编号依次为1 ,2,…,N的长方体积木。对于每块积木,它的三条不同的边分别称为”a边”、“b边”和”c边”,如下图所示:

游戏规则如下:

  1. 从N块积木中选出若干块,并将它们分成M(l<=M<=N)堆,称为第1堆,第2 堆…,第M堆。每堆至少有1块积木,并且第K堆中任意一块积木的编号要大于第K+1堆中任意一块积木的编号(2<=K<=M)。
  2. 对于每一堆积木,游戏者要将它们垂直摞成一根柱子,并要求满足下面两个条件:
    • 除最顶上的一块积木外,任意一块积木的上表面同且仅同另一块积木的下表面接触,并且要求下面的积木的上表面能包含上面的积木的下表面,也就是说,要求下面的积木的上表面的两对边的长度分别大于等于上面的积木的两对边的长度。
    • 对于任意两块上下表面相接触的积木,下面的积木的编号要小于上面的积木的编号。

最后,根据每人所摞成的M根柱子的高度之和来决出胜负。

请你编一程序,寻找一种摞积木的方案,使得你所摞成的M根柱子的高度之和最大。

输入输出

输入文件是INPUT.TXT。文件的第一行有两个正整数N和M(1<=M<=N<=100),分别表示积木总数和要求摞成的柱子数。这两个数之间用一个空格符隔开。接下来N行依次是编号从1到N的N个积木的尺寸,每行有三个1至1000之间的整数,分别表示该积木a边,b边和c边的长度。同一行相邻两个数之间用一个空格符隔开。

输出文件是OUTPUT.TXT。文件只有一行,为一个整数,表示M根柱子的高度之和。

样例

INPUT.TXT

4 2

10 5 5

8 7 7

2 2 2

6 6 6

OUTPUT.TXT

24

题解

好郁闷,这题我竟然连写带调花了将近一天。。。看来我的基本功的确非常不够。

这题对我的启发还是非常大的。lrj书中讨论了递推方向的选择问题,这是我不曾考虑过的。我以前所有的dp题目都是逆推的,状态表示都是“到此情况下为止能获得的最大/最小值”,我觉得这样表示非常自然。但这题行不通。每一个状态都有很多前趋状态,转移方程写起来非常困难。若顺推则容易得多,因为顺推的情况下每一个状态只对应一个后继。但是顺推的思想让我非常不适应(可见我非常菜)。“此情况以后能获得的最值”,这种表示我仔细体会了很长时间才理解并适应,感觉受益匪浅。

当一个问题满足dp所需的一切但是方程写起来非常麻烦,不妨换个递推方向试试吧。

代码(若看不到请设法绕过gfw……orz):

#include <fstream>
using namespace std;
ifstream fin("Input.txt");
ofstream fout("Output.txt");
int N,M;
struct demension{
	unsigned a,b,c;
}demen[101];

int dp[101][101][101][3];
inline int h(int a, int k)
{
	//memo调用的子过程,返回积木a第k面朝上时的高度
	assert(a>0&&a<=N);
	switch(k)
	{
		case 0:	return demen[a].c;
		case 1:	return demen[a].b;
		case 2:	return demen[a].a;
	}
	assert(0);
}
inline bool canput(int a, int k, int aa, int kk)
{
	//memo调用的子过程,返回a的k面能否容纳aa的kk面
	int i,j,ii,jj;
	switch(k)
	{
		case 0:
			i=demen[a].a;
			j=demen[a].b;
			break;
		case 1:
			i=demen[a].a;
			j=demen[a].c;
			break;
		case 2:
			i=demen[a].b;
			j=demen[a].c;
			break;
		default:
			assert(0);
	}
	if(i<j)	swap(i,j);
	switch(kk)
	{
		case 0:
			ii=demen[aa].a;
			jj=demen[aa].b;
			break;
		case 1:
			ii=demen[aa].a;
			jj=demen[aa].c;
			break;
		case 2:
			ii=demen[aa].b;
			jj=demen[aa].c;
			break;
		default:
			assert(0);
	}
	if(ii<jj)	swap(ii,jj);
	return (i>=ii&&j>=jj);
	//这里的题意我开始理解错了,不需要j>=ii。这个错误让我花了一下午调试
}
int memo(int i, int a, int b, int k)
{
	/*已经用前a块积木摆成了i根柱子,顶面积木b的的面k朝上
	之后还能获得的最大高度(决策是否使用a+1块积木、如何使用)*/
	if(dp[i][a][b][k]!=0)	return dp[i][a][b][k];
	if(a==N)	//边界条件
		if(i==M)	return 0;
		else	return INT_MIN;
	int ans=memo(i,a+1,b,k);	//不使用第a+1块积木
	if(i<M)
		for(int kk=0;kk<=2;kk++)
			if(ans<memo(i+1,a+1,a+1,kk)+h(a+1,kk))
				ans=memo(i+1,a+1,a+1,kk)+h(a+1,kk);	//新起一堆
	if(i>0)//这个条件的疏忽让我调试了一早上
		for(int kk=0;kk<=2;kk++)
			if(canput(b,k,a+1,kk)&&ans<memo(i,a+1,a+1,kk)+h(a+1,kk))
				ans=memo(i,a+1,a+1,kk)+h(a+1,kk);	//放在前一堆上
	dp[i][a][b][k]=ans;
	return ans;
}

int main()
{
	fin >> N >> M;
	for(int i=1;i<=N;i++)
		fin >> demen[i].a >> demen[i].b >> demen[i].c;
	demen[0].a=demen[0].b=demen[0].c=1001;
	fout << memo(0,0,0,0) << endl;
	return 0;
}


Tags:,

[NOI99]棋盘分割(Chessboard Division)

Filed Under (OI那一年) by 逆铭 on 2007-05-26 23:08

棋盘分割(NOI99)
Chessboard Division
Chess.{pas|bas|c}
Chess.exe

将一个8×8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)

原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。

请编程对给出的棋盘及n,求出均方差σ的最小值。

输入
第1行为一个整数n(1
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。

输出
仅一个数,为σ(四舍五入精确到小数点后三位)。

样例输入
3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3

样例输出
1.633

题解

现在NOI的题做起来还是很费事……那个sigma的式子的化简则完全是参照lrj的牛书完成的,好失败……

中间调试了相当一段时间,都是由于些弱智错误……囧

#include <fstream>
#include <cassert>
#include <cmath>
#include <iomanip>
using namespace std;
ifstream fin("chess.in");
ofstream fout("chess.out");
int N;

unsigned score[8][8][8][8];
bool scored[8][8][8][8];
unsigned get_score(int x1, int y1, int x2, int y2)
{
   assert(x1<=x2&&y1<=y2);
   if(scored[x1][y1][x2][y2])   return score[x1][y1][x2][y2];
   scored[x1][y1][x2][y2]=true;
   unsigned sum=0;
   for(int x=x1;x<=x2;x++)
      for(int y=y1;y<=y2;y++)
         sum+=score[x][y][x][y];
   score[x1][y1][x2][y2]=sum;
   return sum;
}

unsigned dp[8][8][8][8][15];
bool checked[8][8][8][8][15];
unsigned memo(int x1, int y1, int x2, int y2, int n)
{
   assert(x1<=x2&&y1<=y2);
   if(checked[x1][y1][x2][y2][n])
      return dp[x1][y1][x2][y2][n];
   checked[x1][y1][x2][y2][n]=true;
   if(n==1)
   {
      dp[x1][y1][x2][y2][n]
                =get_score(x1,y1,x2,y2)*get_score(x1,y1,x2,y2);
      return dp[x1][y1][x2][y2][n];
   }

   unsigned ans=INT_MAX;
   for(int x=x1,m;x<x2;x++)
   {
      m=memo(x+1,y1,x2,y2,n-1);
      if(m<INT_MAX)
         ans=min(ans,get_score(x1,y1,x,y2)*get_score(x1,y1,x,y2)
            +memo(x+1,y1,x2,y2,n-1));
      m=memo(x1,y1,x,y2,n-1);
      if(m<INT_MAX)
         ans=min(ans,memo(x1,y1,x,y2,n-1)
            +get_score(x+1,y1,x2,y2)*get_score(x+1,y1,x2,y2));
   }
   for(int y=y1,m;y<y2;y++)
   {
      m=memo(x1,y+1,x2,y2,n-1);
      if(m<UINT_MAX)
         ans=min(ans,get_score(x1,y1,x2,y)*get_score(x1,y1,x2,y)
            +memo(x1,y+1,x2,y2,n-1));
      m=memo(x1,y1,x2,y,n-1);
      if(m<UINT_MAX)
         ans=min(ans,memo(x1,y1,x2,y,n-1)
            +get_score(x1,y+1,x2,y2)*get_score(x1,y+1,x2,y2));
   }
   dp[x1][y1][x2][y2][n]=ans;
   return ans;
}

int main()
{
   fin >> N;
   for(int y=0;y<=7;y++)
      for(int x=0;x<=7;x++)
      {
         fin >> score[x][y][x][y];
         scored[x][y][x][y]=true;
      }
   double x_=double(get_score(0,0,7,7))/N;
   fout << fixed << setprecision(3)
      << sqrt(double(memo(0,0,7,7,N))/N-x_*x_) << endl;
   return 0;
}

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

Search:

Codes, Notes & Scribbles Rss