浅谈随机化算法--模拟退火题目
2016-10-24 21:33
295 查看
一、概述
随机化算法是一种很好的算法,当我们在无法想出正解或者正解很难写的时候。
二、例题
BSOJ1054 LuoguP1054 CODEVS1107 -- 【NOIP 2005提高】等价表达式
Description
明明进了中学之后,学到了代数表达式。有一天,他碰到一个很麻烦的选择题。这个题目的题干中首先给出了一个代数表达式,然后列出了若干选项,每个选项也是一个代数表达式,题目的要求是判断选项中哪些代数表达式是和题干中的表达式等价的。
这个题目手算很麻烦,因为明明对计算机编程很感兴趣,所以他想是不是可以用计算机来解决这个问题。假设你是明明,能完成这个任务吗?
这个选择题中的每个表达式都满足下面的性质:
1.表达式只可能包含一个变量‘a’。
2.表达式中出现的数都是正整数,而且都小于10000。
3.表达式中可以包括四种运算‘+’(加),‘-’(减),‘*’(乘),‘^’(乘幂),以及小括号‘(’,‘)’。小括号的优先级最高,其次是‘^’,然后是‘*’,最后是‘+’和‘-’。‘+’和‘-’的优先级是相同的。相同优先级的运算从左到右进行。 (注意:运算符‘+’,‘-’,‘*’,‘^’以及小括号‘(’,‘)’都是英文字符)
4.幂指数只可能是1到10之间的正整数(包括1和10)。
5.表达式内部,头部或者尾部都可能有一些多余的空格。
下面是一些合理的表达式的例子:
((a^1) ^ 2)^3,a*a+a-a,((a+a)),9999+(a-a)*a,1 + (a -1)^3,1^10^9……
Input
输入的第一行给出的是题干中的表达式。第二行是一个整数n(2 <= n <= 26),表示选项的个数。后面n行,每行包括一个选项中的表达式。这n个选项的标号分别是A,B,C,D……
输入中的表达式的长度都不超过50个字符,而且保证选项中总有表达式和题干中的表达式是等价的。
Output
输出包括一行,这一行包括一系列选项的标号,表示哪些选项是和题干中的表达式等价的。选项的标号按照字母顺序排列,而且之间没有空格。
Sample Input
( a + 1) ^2
3
(a-1)^2+4*a
a + 1+a
a^2 + 2 * a * 1 + 1^2 + 10 -10 +a -a
Sample Output
AC
Hint
对于30%的数据,表达式中只可能出现两种运算符‘+’和‘-’;
对于其它的数据,四种运算符‘+’,‘-’,‘*’,‘^’在表达式中都可能出现。
对于全部的数据,表达式中都可能出现小括号‘(’和‘)’。
显然,这道题使用字符串强行处理是不明智的,那么,我们该怎么做呢?
我们可以随机几个数字(实战告诉我们-1,0,1似乎就可以了)代进去看是否相等。
bzoj2396 POJ3318 神奇的矩阵
Description
给出三个行数和列数均为N的矩阵A、B、C,判断A*B=C是否成立。
Input
题目可能包含若干组数据。
对于每组数据,第一行一个数N,接下来给出三个N*N的矩阵,依次为A、B、C三个矩阵。
Output
对于每组数据,若A*B=C成立,则输出Yes,否则No。每个答案占一行。
Sample Input
1
2
2
100
Sample Output
No
HINT
对于90%的数据,N不超过100;
对于100%的数据,N不超过1000,矩阵中的数字大于等于0小于1000,数据组数不超过5组。
难道又要强上吗?显然O(n^3)的算法是不满足要求的。
那么怎么处理呢?
随机生成一个N乘1的矩阵R,然后判断A*B*R是否等于C*R,而前者相当于A*(B*R),与后者一样都可以在O(N2)的时间里算出来
如果算出来的结果相等,A*B和C几乎也相等。
不放心的话可以多随机几组,但是注意不要超时了。
看了上面几道题,随机化才刚刚入门。
随机化算法在noip等竞赛类考试中比较实用的是 模拟退火 (蚁群和遗传以后有时间再说吧)。
网上对于模拟退火的算法有点繁杂了。
我们首先来看爬山法(也就是贪心)
爬山算法实现很简单,其主要缺点是会陷入局部最优解,而不一定能搜索到全局最优解。如图1所示:假设C点为当前解,爬山算法搜索到A点这个局部最优解就会停止搜索,因为在A点无论向那个方向小幅度移动都不能得到更优的解。
那么模拟退火就可以很好的派上用场了。
模拟退火指的是,在温度较高时(大幅度振动),并有时接受一个比当前最优解较差的解来跳出当前的小山峰,最后在温度越来越低的时候,进行小幅度振动来找到最优解。
我们不妨来看一道例题。
POJ2420 费马点问题求解
题目:http://poj.org/problem?id=2420
题意:给n个点,找出一个点,使这个点到其他所有点的距离之和最小,也就是求费马点。
我们主要来讲一讲模拟退火怎么做这道题。
from ACdreamer
还可以思考以下两道题目
题目:平面上给定n条线段,找出一个点,使这个点到这n条线段的距离和最小。
题目:http://poj.org/problem?id=2069
题意:给定三维空间的n点,找出一个半径最小的球把这些点全部包围住。
做法是相似的。
来看几道题目,都是有其他解法的,但是我们可以运用随机化来简化。
BSOJ2926 RQNOJ595 tyvj 1342 -- 【模拟试题】教主泡嫦娥
Description
【问题背景】
2012年12月21日下午3点14分35秒,全世界各国的总统以及领导人都已经汇聚在中国的方舟上。
但也有很多百姓平民想搭乘方舟,毕竟他们不想就这么离开世界,所以他们决定要么登上方舟,要么毁掉方舟。
LHX教主听说了这件事之后,果断扔掉了手中的船票。在地球即将毁灭的那一霎那,教主自制了一个小型火箭,奔向了月球……
教主登上月球之后才发现,他的女朋友忘记带到月球了,为此他哭了一个月。
但细心的教主立马想起了小学学过的一篇课文,叫做《嫦娥奔月》,于是教主决定,让嫦娥做自己的新任女友。
【问题描述】
教主拿出他最新研制的LHX(Let's be Happy Xixi*^__^*)卫星定位系统,轻松地定位到了广寒宫的位置。
见到嫦娥之后,教主用温柔而犀利的目光瞬间迷倒了嫦娥,但嫦娥也想考验一下教主。
嫦娥对教主说:“看到那边的环形山了么?你从上面那个环走一圈我就答应你~”
教主用LHX卫星定位系统查看了环形山的地形,环形山上一共有N个可以识别的落脚点,以顺时针1~N编号。每个落脚点都有一个海拔,相邻的落脚点海拔不同(第1个和第N个相邻)。
教主可以选择从任意一个落脚点开始,顺时针或者逆时针走,每次走到一个相邻的落脚点,并且最后回到这个落脚点。
教主在任意时刻,都会有“上升”、“下降”两种状态的其中一种。
当教主从第i个落脚点,走到第j个落脚点的时候(i和j相邻)
j的海拔高于i的海拔:如果教主处于上升状态,教主需要耗费两段高度差的绝对值的体力;否则耗费高度差平方的体力。
j的海拔低于i的海拔:如果教主处于下降状态,教主需要耗费两段高度差的绝对值的体力;否则耗费高度差平方的体力。
当然,教主可以在到达一个落脚点的时候,选择切换自己的状态(上升→下降,下降→上升),每次切换需要耗费M点的体力。在起点的时候,教主可以自行选择状态并且不算切换状态,也就是说刚开始教主可以选择任意状态并且不耗费体力。
教主希望花费最少的体力,让嫦娥成为自己的女朋友。
Input
输入的第一行为两个正整数N与M,即落脚点的个数与切换状态所消耗的体力。
接下来一行包含空格隔开的N个正整数,表示了每个落脚点的高度,题目保证了相邻落脚点高度不相同。
Output
输出仅包含一个正整数,即教主走一圈所需消耗的最小体力值。
注意:C++选手建议使用cout输出long long类型整数。
Sample Input
6 7
4 2 6 2 5 6
Sample Output
27
Hint
【样例解释】
从第3个落脚点开始以下降状态向前走,并在第4个落脚点时切换为上升状态。这样共耗费4 +(7)+3+1+2^2+2^2+4=27点体力。
【数据范围】
对于10%的数据,N ≤ 10;
对于30%的数据,N ≤ 100,a[i] ≤ 1000;
对于50%的数据,N ≤ 1000,a[i] ≤ 100000;
对于100%的数据,N ≤ 10000,a[i] ≤ 1000000,M ≤ 1000000000;
可以很简单地看出化环为链,枚举起点的暴力DP方法,方程也是显而易见的,这里不多做赘述。
但是数据范围不允许这种O(n*n)的算法AC。
可以注意到,这个算法的缓慢之处在于枚举起点。
怎么加快这个“枚举”呢?
我请教了同期随机化大神,这个枚举可通过以下随机化方案解决:
先随机20个起点,对于每个随机的起点,进行多(30+)次优化,所谓优化,就是以这个起点为中心,左右随机其他的点,以这个“其他的点”来优化当前“圆心”,随着时间的推移,优化的范围逐渐减小。
具体代码是这样的:
now=tp[i]+sin((double)(rand()%10000/10000))*T;
(其实就是斜边*正弦=直角边)
这里的T就是长度,T随着每一轮优化减小(T*=0.x)。
这样就能快速覆盖整个数列,并且准确度较高,速度较快地完成对起点的枚举。
代码如下。
BSOJ2684 cogs 362 -- 【CEOI2004】锯木厂选址
Description
从山顶上到山底下沿着一条直线种植了n棵老树。当地的政府决定把他们砍下来。为了不浪费任何一棵木材,树被砍倒后要运送到锯木厂。
木材只能按照一个方向运输:朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建两个锯木厂,使得传输的费用总和最小。假定运输每公斤木材每米需要一分钱。
你的任务是编写一个程序,从输入文件中读入树的个数和他们的重量与位置,计算最小运输费用。
Input
输入的第一行为一个正整数n——树的个数(2≤n≤20000)。树从山顶到山脚按照1,2……n标号。接下来n行,每行有两个正整数(用空格分开)。第i+1行含有:wi——第i棵树的重量(公斤为单位)和 di——第i棵树和第i+1棵树之间的距离,1≤wi≤10000,0≤di≤10000。最后一个数dn,表示第n棵树到山脚的锯木厂的距离。保证所有树运到山脚的锯木厂所需要的费用小于2000000000分。
Output
输出只有一行一个数:最小的运输费用。
Sample Input
9
1 2
2 1
3 3
1 1
3 2
1 6
2 1
1 2
1 1
Sample Output
26
这是一道老题了。
但是在做这道题的时候,没有使用接受较差解,而是像代码中这样写的。
注意模拟退火是一定要接受一个较差解的,下面有点乱搞了
正解是斜率优化+DP
刘家骅在2008年国家队论文《浅谈随机化在信息学竞赛中的应用》中仔细讲解了这道题,但是他的代码我找不到了。
分析:
这道题目的标准算法将数据转化为图象,用栈进行处理求出两个矩形的最大覆盖面积,时间复杂度为O(N)。但是,这种算法对能力要求不小,不太容易想到。
我们看下随机化算法在这题上的表现。
首先最容易想到的随机化当然就是直接随机寻找两个点,计算出以这两个点为锯木场时的总运费,多次随机后将总费用最小的输出。
我们可以进行预处理,将计算的时间复杂度降为O(1),那么在时限内我们可以随机几百万次甚至几千万次,但是相对于总状态的四亿来说,寻找到最优解的几率不是很大。
有没有更好的方法呢?
我们刚才是用随机化算法直接出解,准确性不太好,为了增加准确性,那么我们尝试一下用随机化来缩小区域范围。
我们建立一个矩阵P,P[X,Y]表示第一个锯木场建立在X,第二个锯木场建立在Y时的总运费。一开始时,矩阵的边长为N。我们随机寻找一定数量的点(如下左图所示,取点数量应该充分利用时限并且注意效率,由于矩阵的大小一直在变化,推荐使用矩阵大小的定比确定取点数量),计算出它们的值,取其最小点,以这个点为新矩阵的中心,以现在矩阵的边长的3/4的长度为新矩形的边长(如下右图所示),从原来的矩阵中取出一块作为新矩阵的范围(若新矩阵的范围出了原矩阵的边界就将其向里移动到原矩阵内),然后继续在新矩阵中重复这样的操作,直至新矩阵足够小时,我们即可枚举新矩阵上的每一个点,取其中最小值作为答案。
我们惊喜地发现,这种随机化算法对于测试数据能够全部通过!
通过这道题目可以看出,随机化算法的灵活多变使得它的具有更为广阔的运用范围,在许多看似难以入手的地方通过巧妙地运用发光发热。而这样的多变性也使得我们需要灵活恰当地运用随机化算法才能发挥出它的优势。随机化算法并不只是简单地随便乱来,使用随机化算法的时候与其他算法一样值得细细斟酌,需要匠心独运。
最后再来一道题目,并不是模拟退火。
BSOJ4874 -- 【NOIP2016-4】 排序 (noip模拟)
Description
shell 排序是众多排序算法中的一种。给定 N 个整数,存放在数组 A 中,排成升序。下表是两种不同语言的排序程序代码段:
此处的 i, N, X, gap, temp, ok 均是整数。数组 A 的元素互不相同,取值范围在 1~N 之间。如果第 11 行被遗漏了,这个有 bug 的 shell 排序程序在 X 取某些值时,仍然有可能得到正确的排序结果。
请你找出所有能得到正确排序结果的 X。
Input
第1 行:1 个整数 N,表示要排序的元素个数
第2 行:N 个整数,表示要排序的数组 A,保证初始状态非升序。
Output
第1 行:1 个整数 C,表示 X 有多少种不同的取值方案,能得到正确排序结果
第2 行:C 个整数,表示能得到正确排序结果的 X 的取值,升序排列
Sample Input
6
4 2 6 1 5 3
Sample Output
2
1 3
Hint
[样例说明]
有 6 个元素,分别是 4, 2, 6, 1, 5, 3。能得到正确的排序结果的 X 有 2 种取值方案:
•X = 1, 我们交换以下这些位置的元素
(1,2), (3,4), (4,5), (5,6), (2,3), (4,5), (1,2), (3,4);
•X = 3, 我们交换以下这些位置的元素
(1,4), (3,6).
[数据说明]
•1 < N <= 500000
•1 ≤ X ≤ N-1
先给随机化乱搞。
正解如下,有一个归位的思想即可。
随机化算法是一种很好的算法,当我们在无法想出正解或者正解很难写的时候。
二、例题
BSOJ1054 LuoguP1054 CODEVS1107 -- 【NOIP 2005提高】等价表达式
Description
明明进了中学之后,学到了代数表达式。有一天,他碰到一个很麻烦的选择题。这个题目的题干中首先给出了一个代数表达式,然后列出了若干选项,每个选项也是一个代数表达式,题目的要求是判断选项中哪些代数表达式是和题干中的表达式等价的。
这个题目手算很麻烦,因为明明对计算机编程很感兴趣,所以他想是不是可以用计算机来解决这个问题。假设你是明明,能完成这个任务吗?
这个选择题中的每个表达式都满足下面的性质:
1.表达式只可能包含一个变量‘a’。
2.表达式中出现的数都是正整数,而且都小于10000。
3.表达式中可以包括四种运算‘+’(加),‘-’(减),‘*’(乘),‘^’(乘幂),以及小括号‘(’,‘)’。小括号的优先级最高,其次是‘^’,然后是‘*’,最后是‘+’和‘-’。‘+’和‘-’的优先级是相同的。相同优先级的运算从左到右进行。 (注意:运算符‘+’,‘-’,‘*’,‘^’以及小括号‘(’,‘)’都是英文字符)
4.幂指数只可能是1到10之间的正整数(包括1和10)。
5.表达式内部,头部或者尾部都可能有一些多余的空格。
下面是一些合理的表达式的例子:
((a^1) ^ 2)^3,a*a+a-a,((a+a)),9999+(a-a)*a,1 + (a -1)^3,1^10^9……
Input
输入的第一行给出的是题干中的表达式。第二行是一个整数n(2 <= n <= 26),表示选项的个数。后面n行,每行包括一个选项中的表达式。这n个选项的标号分别是A,B,C,D……
输入中的表达式的长度都不超过50个字符,而且保证选项中总有表达式和题干中的表达式是等价的。
Output
输出包括一行,这一行包括一系列选项的标号,表示哪些选项是和题干中的表达式等价的。选项的标号按照字母顺序排列,而且之间没有空格。
Sample Input
( a + 1) ^2
3
(a-1)^2+4*a
a + 1+a
a^2 + 2 * a * 1 + 1^2 + 10 -10 +a -a
Sample Output
AC
Hint
对于30%的数据,表达式中只可能出现两种运算符‘+’和‘-’;
对于其它的数据,四种运算符‘+’,‘-’,‘*’,‘^’在表达式中都可能出现。
对于全部的数据,表达式中都可能出现小括号‘(’和‘)’。
显然,这道题使用字符串强行处理是不明智的,那么,我们该怎么做呢?
我们可以随机几个数字(实战告诉我们-1,0,1似乎就可以了)代进去看是否相等。
#include<iostream> #include<iomanip> #include<cstring> #include<cmath> #include<cstdio> #define mod 10003 using namespace std; int j,f[15],n,k; string s,x; int find_number(string s,int l,int r,int &t) { t=0; for(int i=l;i<=r;i++) { if(s[i]<'0'||s[i]>'9')return 0; else t=t*10+s[i]-'0'; } return 1; } int find_low(string s,int l,int r) { int k=0,t=0,j=0; for(int i=r;i>=l;i--) { if(s[i]=='a'||(s[i]<='9'&&s[i]>='0'))continue; if(s[i]==')'){t++;continue;} if(s[i]=='('){t--;continue;} if((s[i]=='+'||s[i]=='-')&&t==0)return i; if(!k&&!t&&s[i]=='*'){k=i;continue;} if(!j&&!t&&s[i]=='^'){j=i;continue;} } if(!k)return j; return k; } int cal(int a,char ch,int b){ int t=1; if(ch=='+') return ((a+b)%mod+mod)%mod; if(ch=='-') return ((a-b)%mod+mod)%mod; if(ch=='*') return ((a*b)%mod+mod)%mod; if(ch=='^') for(int i=1;i<=b;i++) t=((a*t)%mod+mod)%mod; return t; } int matchr(string s,int l,int r) { int tot=1; for(int i=l+1;i<=r;i++) { if(s[i]=='(')tot++; else if(s[i]==')')tot--; if(tot==0)return i; } } void delspace(string &s) { string tmp=" "; int len=s.length(); for(int i=0;i<len;i++) if(s[i]!=' ')tmp+=s[i]; s=tmp; } int t(string s,int l,int r) { int pos,left,right,k; if(s[l]=='('&&matchr(s,l,r)==r)return t(s,l+1,r-1); if(find_number(s,l,r,k))return k; if(s[l]=='a'&&l==r)return j; pos=find_low(s,l,r); left=t(s,l,pos-1); right=t(s,pos+1,r); k=cal(left,s[pos],right); return k; } int main(){ getline(cin,s); delspace(s); cin>>n; for(j=0;j<=4;j++)f[j]=t(s,1,s.length()-1); getline(cin,x); for(int i=1;i<=n;i++) { getline(cin,x); delspace(x); for(j=0;j<=4;j++) { k=t(x,1,x.length()-1); if(k!=f[j])break; } if(j>4)cout<<char(i+'A'-1); } return 0; }
bzoj2396 POJ3318 神奇的矩阵
Description
给出三个行数和列数均为N的矩阵A、B、C,判断A*B=C是否成立。
Input
题目可能包含若干组数据。
对于每组数据,第一行一个数N,接下来给出三个N*N的矩阵,依次为A、B、C三个矩阵。
Output
对于每组数据,若A*B=C成立,则输出Yes,否则No。每个答案占一行。
Sample Input
1
2
2
100
Sample Output
No
HINT
对于90%的数据,N不超过100;
对于100%的数据,N不超过1000,矩阵中的数字大于等于0小于1000,数据组数不超过5组。
难道又要强上吗?显然O(n^3)的算法是不满足要求的。
那么怎么处理呢?
随机生成一个N乘1的矩阵R,然后判断A*B*R是否等于C*R,而前者相当于A*(B*R),与后者一样都可以在O(N2)的时间里算出来
如果算出来的结果相等,A*B和C几乎也相等。
不放心的话可以多随机几组,但是注意不要超时了。
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; int n; int a[1001][1001],b[1001][1001],c[1001][1001]; int rnd[1001],ans1[1001],ans2[1001]; void mmul(int a[1001],int b[1001][1001],int s[1001]) { int tmp[1001]; for(int j=1;j<=n;j++) { tmp[j]=0; for(int k=1;k<=n;k++) tmp[j]+=a[k]*b[k][j]; } for(int i=1;i<=n;i++)s[i]=tmp[i]; } bool jud() { for(int i=1;i<=n;i++) if(ans1[i]!=ans2[i])return 0; return 1; } int main() { for(int i=1;i<=1000;i++)rnd[i]=rand(); while(scanf("%d",&n)!=EOF) { for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&a[i][j]); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&b[i][j]); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&c[i][j]); mmul(rnd,a,ans1); mmul(ans1,b,ans1); mmul(rnd,c,ans2); if(jud())printf("Yes\n"); else printf("No\n"); } return 0; }
看了上面几道题,随机化才刚刚入门。
随机化算法在noip等竞赛类考试中比较实用的是 模拟退火 (蚁群和遗传以后有时间再说吧)。
网上对于模拟退火的算法有点繁杂了。
我们首先来看爬山法(也就是贪心)
爬山算法实现很简单,其主要缺点是会陷入局部最优解,而不一定能搜索到全局最优解。如图1所示:假设C点为当前解,爬山算法搜索到A点这个局部最优解就会停止搜索,因为在A点无论向那个方向小幅度移动都不能得到更优的解。
那么模拟退火就可以很好的派上用场了。
模拟退火指的是,在温度较高时(大幅度振动),并有时接受一个比当前最优解较差的解来跳出当前的小山峰,最后在温度越来越低的时候,进行小幅度振动来找到最优解。
我们不妨来看一道例题。
POJ2420 费马点问题求解
题目:http://poj.org/problem?id=2420
题意:给n个点,找出一个点,使这个点到其他所有点的距离之和最小,也就是求费马点。
我们主要来讲一讲模拟退火怎么做这道题。
from ACdreamer
#include <iostream> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <time.h> #include <math.h> #define N 1005 #define eps 1e-8 //搜索停止条件阀值 #define INF 1e99 #define delta 0.98 //温度下降速度 #define T 100 //初始温度 using namespace std; int dx[4] = {0, 0, -1, 1}; int dy[4] = {-1, 1, 0, 0}; //上下左右四个方向 struct Point { double x, y; }; Point p ; double dist(Point A, Point B) { return sqrt((A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y)); } double GetSum(Point p[], int n, Point t) { double ans = 0; while(n--) ans += dist(p , t); return ans; } //其实我觉得这玩意儿根本不叫模拟退火 double Search(Point p[], int n) { Point s = p[0]; //随机初始化一个点开始搜索 double t = T; //初始化温度 double ans = INF; //初始答案值 while(t > eps) { bool flag = 1; while(flag) { flag = 0; for(int i = 0; i < 4; i++) //上下左右四个方向 { Point z; z.x = s.x + dx[i] * t; z.y = s.y + dy[i] * t; double tp = GetSum(p, n, z); if(ans > tp) { ans = tp; s = z; flag = 1; } } } t *= delta; } return ans; } int main() { int n; while(scanf("%d", &n) != EOF) { for(int i = 0; i < n; i++) scanf("%lf %lf", &p[i].x, &p[i].y); printf("%.0lf\n", Search(p, n)); } return 0; }
还可以思考以下两道题目
题目:平面上给定n条线段,找出一个点,使这个点到这n条线段的距离和最小。
题目:http://poj.org/problem?id=2069
题意:给定三维空间的n点,找出一个半径最小的球把这些点全部包围住。
做法是相似的。
来看几道题目,都是有其他解法的,但是我们可以运用随机化来简化。
BSOJ2926 RQNOJ595 tyvj 1342 -- 【模拟试题】教主泡嫦娥
Description
【问题背景】
2012年12月21日下午3点14分35秒,全世界各国的总统以及领导人都已经汇聚在中国的方舟上。
但也有很多百姓平民想搭乘方舟,毕竟他们不想就这么离开世界,所以他们决定要么登上方舟,要么毁掉方舟。
LHX教主听说了这件事之后,果断扔掉了手中的船票。在地球即将毁灭的那一霎那,教主自制了一个小型火箭,奔向了月球……
教主登上月球之后才发现,他的女朋友忘记带到月球了,为此他哭了一个月。
但细心的教主立马想起了小学学过的一篇课文,叫做《嫦娥奔月》,于是教主决定,让嫦娥做自己的新任女友。
【问题描述】
教主拿出他最新研制的LHX(Let's be Happy Xixi*^__^*)卫星定位系统,轻松地定位到了广寒宫的位置。
见到嫦娥之后,教主用温柔而犀利的目光瞬间迷倒了嫦娥,但嫦娥也想考验一下教主。
嫦娥对教主说:“看到那边的环形山了么?你从上面那个环走一圈我就答应你~”
教主用LHX卫星定位系统查看了环形山的地形,环形山上一共有N个可以识别的落脚点,以顺时针1~N编号。每个落脚点都有一个海拔,相邻的落脚点海拔不同(第1个和第N个相邻)。
教主可以选择从任意一个落脚点开始,顺时针或者逆时针走,每次走到一个相邻的落脚点,并且最后回到这个落脚点。
教主在任意时刻,都会有“上升”、“下降”两种状态的其中一种。
当教主从第i个落脚点,走到第j个落脚点的时候(i和j相邻)
j的海拔高于i的海拔:如果教主处于上升状态,教主需要耗费两段高度差的绝对值的体力;否则耗费高度差平方的体力。
j的海拔低于i的海拔:如果教主处于下降状态,教主需要耗费两段高度差的绝对值的体力;否则耗费高度差平方的体力。
当然,教主可以在到达一个落脚点的时候,选择切换自己的状态(上升→下降,下降→上升),每次切换需要耗费M点的体力。在起点的时候,教主可以自行选择状态并且不算切换状态,也就是说刚开始教主可以选择任意状态并且不耗费体力。
教主希望花费最少的体力,让嫦娥成为自己的女朋友。
Input
输入的第一行为两个正整数N与M,即落脚点的个数与切换状态所消耗的体力。
接下来一行包含空格隔开的N个正整数,表示了每个落脚点的高度,题目保证了相邻落脚点高度不相同。
Output
输出仅包含一个正整数,即教主走一圈所需消耗的最小体力值。
注意:C++选手建议使用cout输出long long类型整数。
Sample Input
6 7
4 2 6 2 5 6
Sample Output
27
Hint
【样例解释】
从第3个落脚点开始以下降状态向前走,并在第4个落脚点时切换为上升状态。这样共耗费4 +(7)+3+1+2^2+2^2+4=27点体力。
【数据范围】
对于10%的数据,N ≤ 10;
对于30%的数据,N ≤ 100,a[i] ≤ 1000;
对于50%的数据,N ≤ 1000,a[i] ≤ 100000;
对于100%的数据,N ≤ 10000,a[i] ≤ 1000000,M ≤ 1000000000;
可以很简单地看出化环为链,枚举起点的暴力DP方法,方程也是显而易见的,这里不多做赘述。
但是数据范围不允许这种O(n*n)的算法AC。
可以注意到,这个算法的缓慢之处在于枚举起点。
怎么加快这个“枚举”呢?
我请教了同期随机化大神,这个枚举可通过以下随机化方案解决:
先随机20个起点,对于每个随机的起点,进行多(30+)次优化,所谓优化,就是以这个起点为中心,左右随机其他的点,以这个“其他的点”来优化当前“圆心”,随着时间的推移,优化的范围逐渐减小。
具体代码是这样的:
now=tp[i]+sin((double)(rand()%10000/10000))*T;
(其实就是斜边*正弦=直角边)
这里的T就是长度,T随着每一轮优化减小(T*=0.x)。
这样就能快速覆盖整个数列,并且准确度较高,速度较快地完成对起点的枚举。
代码如下。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cstdlib> #include<cmath> #include<ctime> using namespace std; typedef long long ll; inline ll read() { ll bj=1ll; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-')bj=-1ll; ch=getchar(); } ll ret=0ll; while(ch>='0'&&ch<='9')ret=ret*10ll+ch-'0',ch=getchar(); return ret*bj; } ll a[10001*2]={0},f[30001][2]={0},n,m,minn=1e100,b[30001]={0},cnt=0,tp[105]={0},note[100004]={0},now; double T; ll square(ll x){return x*x;} ll DP(ll st) { if(note[st])return note[st]; cnt=0; for(ll i=st;i<=st+n;i++)b[++cnt]=a[i]; f[1][1]=f[1][0]=0;//1:up 0:down for(ll i=2;i<=n+1;i++) { if(b[i]>b[i-1]) { f[i][1]=min(f[i-1][0]+m+b[i]-b[i-1],f[i-1][1]+b[i]-b[i-1]); f[i][0]=min(f[i-1][1]+m+square(b[i]-b[i-1]),f[i-1][0]+square(b[i]-b[i-1])); } else { f[i][0]=min(f[i-1][1]+m+b[i-1]-b[i],f[i-1][0]+b[i-1]-b[i]); f[i][1]=min(f[i-1][0]+m+square(b[i]-b[i-1]),f[i-1][1]+square(b[i]-b[i-1])); } } return note[st]=min(f[n+1][1],f[n+1][0]); } int main() { srand(time(NULL)); n=read();m=read(); for(ll i=1;i<=n;i++)a[i+n]=a[i]=read(); for(int i=1;i<=20;i++)tp[i]=rand()%n+1; T=n; while(T>=1) { for(int i=1;i<=20;i++) { for(int j=1;j<=30;j++) { now=tp[i]+sin((double)(rand()%10000)/10000)*T; if(now<=0||now>n||DP(now)>=DP(tp[i]))continue; tp[i]=now; } } T*=0.5; } for(int i=1;i<=20;i++)minn=min(minn,note[tp[i]]); printf("%lld",minn); return 0; }
BSOJ2684 cogs 362 -- 【CEOI2004】锯木厂选址
Description
从山顶上到山底下沿着一条直线种植了n棵老树。当地的政府决定把他们砍下来。为了不浪费任何一棵木材,树被砍倒后要运送到锯木厂。
木材只能按照一个方向运输:朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建两个锯木厂,使得传输的费用总和最小。假定运输每公斤木材每米需要一分钱。
你的任务是编写一个程序,从输入文件中读入树的个数和他们的重量与位置,计算最小运输费用。
Input
输入的第一行为一个正整数n——树的个数(2≤n≤20000)。树从山顶到山脚按照1,2……n标号。接下来n行,每行有两个正整数(用空格分开)。第i+1行含有:wi——第i棵树的重量(公斤为单位)和 di——第i棵树和第i+1棵树之间的距离,1≤wi≤10000,0≤di≤10000。最后一个数dn,表示第n棵树到山脚的锯木厂的距离。保证所有树运到山脚的锯木厂所需要的费用小于2000000000分。
Output
输出只有一行一个数:最小的运输费用。
Sample Input
9
1 2
2 1
3 3
1 1
3 2
1 6
2 1
1 2
1 1
Sample Output
26
这是一道老题了。
但是在做这道题的时候,没有使用接受较差解,而是像代码中这样写的。
注意模拟退火是一定要接受一个较差解的,下面有点乱搞了
正解是斜率优化+DP
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> using namespace std; typedef long long ll; ll n,w[20007]={0},d[20007]={0},sd[20007]={0},sum[20007]={0},co[20007]={0}; ll rx[209]={0},ry[209]={0},ans[209]={0}; inline ll get(ll i,ll j) {return co[n+1]-sum[j]*(sd[i]-sd[j])-sum[i]*(sd[n+1]-sd[i]);} void solve() { double T=n*1.1; while(T>1) { for(ll i=1;i<=30;i++) { for(ll j=1;j<=80;j++) { ll nx=(ll)rx[i]*(1-double((rand()%(ll(T+0.5)+1))/double(n))+double((rand()%(ll(T+0.5)+1))/double(n))); ll ny=(ll)ry[i]*(1-double((rand()%(ll(T+0.5)+1))/double(n))+double((rand()%(ll(T+0.5)+1))/double(n))); while(ny==nx) ny=(ll)ry[i]*(1-double((rand()%(ll(T+0.5)+1))/double(n))+double((rand()%(ll(T+0.5)+1))/double(n))); if(nx<1||nx>n||ny<1||ny>n) continue; ll tmp=get(nx,ny); if(tmp<ans[i]) { ans[i]=tmp; rx[i]=nx; ry[i]=ny; } } } T*=0.2; } } int main() { scanf("%lld",&n); srand(time(NULL)); for(ll i=1;i<=n;i++) { scanf("%lld%lld",&w[i],&d[i]); sd[i]=sd[i-1]+d[i-1]; sum[i]=sum[i-1]+w[i]; co[i]=co[i-1]+d[i-1]*sum[i-1]; } sum[n+1]=sum ; sd[n+1]=sd +d ; co[n+1]=co +sum *d ; for(ll i=1;i<=80;i++) { rx[i]=rand()%n+1; ry[i]=rand()%n+1; while(ry[i]==rx[i]) ry[i]=rand()%n+1; ans[i]=get(rx[i],ry[i]); } solve(); ll minn=1e100; for(ll i=1;i<=30;i++) minn=min(minn,ans[i]); printf("%lld\n",minn); return 0; }
刘家骅在2008年国家队论文《浅谈随机化在信息学竞赛中的应用》中仔细讲解了这道题,但是他的代码我找不到了。
分析:
这道题目的标准算法将数据转化为图象,用栈进行处理求出两个矩形的最大覆盖面积,时间复杂度为O(N)。但是,这种算法对能力要求不小,不太容易想到。
我们看下随机化算法在这题上的表现。
首先最容易想到的随机化当然就是直接随机寻找两个点,计算出以这两个点为锯木场时的总运费,多次随机后将总费用最小的输出。
我们可以进行预处理,将计算的时间复杂度降为O(1),那么在时限内我们可以随机几百万次甚至几千万次,但是相对于总状态的四亿来说,寻找到最优解的几率不是很大。
有没有更好的方法呢?
我们刚才是用随机化算法直接出解,准确性不太好,为了增加准确性,那么我们尝试一下用随机化来缩小区域范围。
我们建立一个矩阵P,P[X,Y]表示第一个锯木场建立在X,第二个锯木场建立在Y时的总运费。一开始时,矩阵的边长为N。我们随机寻找一定数量的点(如下左图所示,取点数量应该充分利用时限并且注意效率,由于矩阵的大小一直在变化,推荐使用矩阵大小的定比确定取点数量),计算出它们的值,取其最小点,以这个点为新矩阵的中心,以现在矩阵的边长的3/4的长度为新矩形的边长(如下右图所示),从原来的矩阵中取出一块作为新矩阵的范围(若新矩阵的范围出了原矩阵的边界就将其向里移动到原矩阵内),然后继续在新矩阵中重复这样的操作,直至新矩阵足够小时,我们即可枚举新矩阵上的每一个点,取其中最小值作为答案。
我们惊喜地发现,这种随机化算法对于测试数据能够全部通过!
通过这道题目可以看出,随机化算法的灵活多变使得它的具有更为广阔的运用范围,在许多看似难以入手的地方通过巧妙地运用发光发热。而这样的多变性也使得我们需要灵活恰当地运用随机化算法才能发挥出它的优势。随机化算法并不只是简单地随便乱来,使用随机化算法的时候与其他算法一样值得细细斟酌,需要匠心独运。
最后再来一道题目,并不是模拟退火。
BSOJ4874 -- 【NOIP2016-4】 排序 (noip模拟)
Description
shell 排序是众多排序算法中的一种。给定 N 个整数,存放在数组 A 中,排成升序。下表是两种不同语言的排序程序代码段:
此处的 i, N, X, gap, temp, ok 均是整数。数组 A 的元素互不相同,取值范围在 1~N 之间。如果第 11 行被遗漏了,这个有 bug 的 shell 排序程序在 X 取某些值时,仍然有可能得到正确的排序结果。
请你找出所有能得到正确排序结果的 X。
Input
第1 行:1 个整数 N,表示要排序的元素个数
第2 行:N 个整数,表示要排序的数组 A,保证初始状态非升序。
Output
第1 行:1 个整数 C,表示 X 有多少种不同的取值方案,能得到正确排序结果
第2 行:C 个整数,表示能得到正确排序结果的 X 的取值,升序排列
Sample Input
6
4 2 6 1 5 3
Sample Output
2
1 3
Hint
[样例说明]
有 6 个元素,分别是 4, 2, 6, 1, 5, 3。能得到正确的排序结果的 X 有 2 种取值方案:
•X = 1, 我们交换以下这些位置的元素
(1,2), (3,4), (4,5), (5,6), (2,3), (4,5), (1,2), (3,4);
•X = 3, 我们交换以下这些位置的元素
(1,4), (3,6).
[数据说明]
•1 < N <= 500000
•1 ≤ X ≤ N-1
先给随机化乱搞。
#include<iostream> #include<iomanip> #include<cstring> #include<cstdlib> #include<cmath> #include<ctime> #include<cstdio> using namespace std; int n; int r[500009]={0}; int fir[109]={0}; int sec[109]={0}; bool possible[500009]={0}; void Get_firsec(int x,int i) { int yao=0x3fffffff,er=0x3fffffff; while(x<=n) { if(r[x]<yao) { er=yao; yao=r[x]; x+=i; continue; } er=min(er,r[x]); x+=i; } fir[0]++; fir[fir[0]]=yao; sec[0]++; sec[sec[0]]=er; } void Solve_belief() { srand(time(NULL)); int pos,t; bool ok; for(int i=n;i>=1;i--) { if(possible[i])continue; fir[0]=0; sec[0]=0; ok=1; t=min(100,i); for(int j=0;j<=t-1;j++) { pos=(rand()%(i/t))+1; pos=j*(i/t)+pos; Get_firsec(pos,i); if(j>=1) { if(fir[j]>fir[j+1]){ok=0;break;} if(sec[j]>sec[j+1]){ok=0;break;} } } if(ok==0)continue; possible[i]=1; for(int j=1;j*j<=i;j++) { if(i%j==0) { possible[j]=1; possible[i/j]=1; } } } int cnt=0; for(int i=1;i<=n;i++)if(possible[i])cnt++; cout<<cnt<<endl; for(int i=1;i<=n;i++)if(possible[i])printf("%d ",i); } int main(){ cin>>n; for(int i=1;i<=n;i++)scanf("%d",&r[i]); Solve_belief(); fclose(stdout); return 0; }
正解如下,有一个归位的思想即可。
#include<iostream> #include<iomanip> #include<cstring> #include<algorithm> #include<cstdio> using namespace std; int n,cnt=0,h[500005],c1=0; inline int gcd(int a,int b) { if(b==0) return a; return gcd(b,a%b); } int main(){ cin>>n; int x; int G=0; for(int i=1;i<=n;i++) { scanf("%d",&x); if(x!=i) { if(!G) G=abs(x-i); else G=gcd(G,abs(x-i)); } } int c1=0; for(int i=1;i<=G;i++) if(G%i==0)c1++,h[c1]=i; sort(h+1,h+c1+1); cout<<c1<<endl; for(int i=1;i<=c1;i++) cout<<h[i]<<" "; return 0; }
相关文章推荐
- ACM 模拟退火,DLX题目集合
- [AI]模拟退火解决TSP问题(含源码)
- HDU 4488Faulhaber’s Triangle(模拟 题目有公式)
- hdu 2899 Strange fuction 模拟退火
- POJ 2420 A Star not a Tree? 费马点 计算几何 模拟退火
- POJ 1379 Run away & POJ 2420 A star not a Tree [模拟退火] [爬山算法]
- HDU 3007 Buried memory(点集最小圆覆盖 模拟退火解法)
- Berland National Library(模拟题目)
- 模拟题目HDU2599
- 局部搜索,模拟退火,遗传算法,禁忌搜索的形象比喻
- hdu 3644(几何模拟退火
- POJ题目分类---模拟[A一道删一道]
- 【模拟退火】POJ 2420/ZOJ 1901 费马点
- BZOJ 3680 吊打XXX 计算几何 模拟退火 广义费马点
- NOJ 题目1645 聊天止于呵呵(模拟)
- bzoj3680 -- 模拟退火
- CodeForces 25BPhone numbers(简单的字符串模拟题目)
- 数据结构上机题目--离散时间模拟(银行等待问题)
- 争取几句话描述一下爬山法,模拟退火,遗传算法
- 【转】大话模拟退火