[NOI2014]随机数生成器 题解

原题网址:
bzoj P3671
洛谷 P2354
附原题:

时间限制 内存限制
1.00s ~ 5.00s 250.00MB

题目描述

小 H 最近在研究随机算法。随机算法往往需要通过调用随机数生成函数(例如 Pascal 中的 random 和 C/C++中的 rand)来获得随机性。事实上,随机数生成函数也并不是真正的“随机”,其一般都是利用某个算法计算得来的。
比如,下面这个二次多项式递推算法就是一个常用算法:
算法选定非负整数\(x_0,a,b,c,d\)作为随机种子,并采用如下递推公式进行计算。
对于任意\(i \geq 1\) $x_i==(a×x+b×x_{i-1}^2+c) mod \(d\) 这样可以得到一个任意长度的非负整数数列 \({x_i,i\geq1}\),一般来说,我们认为这个数列是随机的。
利用随机序列\({xi},i\geq1\),我们还可以采用如下算法来产生一个\(1\)到 \(K\) 的随机排列\({T_i},i=1...k\)
1、初始设\(T\)为\(1\)到\(K\)的递增序列;
2、对\(T\)进行\(K\)次交换,第\(i\)次交换,交换\(T_i\)和\(T_{x_i}\)mod \(_{i+1}\)
此外,小 H 在这\(K\)次交换的基础上,又额外进行了\(Q\)次交换操作,对于第i 次额外交换,小 H 会选定两个下标\(u_i和v_i\),并交换\(T_{v_i}和T_{v_i}\)的值
为了检验这个随机排列生成算法的实用性,小 H 设计了如下问题:
小 H 有一个 N 行 M 列的棋盘,她首先按照上述过程,通过\(N \times M + Q\)次交换操作,生成了一个\(1\sim N \times M∼N\times M\)的随机排列,\(T_i,i=1...N \times M\)。然后将这\(N \times M\)个数逐行逐列依次填入这个棋盘:也就是第\(i\)行第\(j\)列的格子上所填入的数应为\(T_{(i-1)\times M+u_j}\)。
接着小 H 希望从棋盘的左上角,也就是第一行第一列的格子出发,每次向右走或者向下走,在不走出棋盘的前提下,走到棋盘的右下角,也就是第 \(N\)行第\(M\)列的格子。
小 H 把所经过格子上的数字都记录了下来,并从小到大排序,这样,对于任何一条合法的移动路径,小 H 都可以得到一个长度为\(N + M - 1\)的升序序列,我们称之为路径序列。
小 H 想知道,她可能得到的字典序最小的路径序列应该是怎样的呢?

输入格式

第1行包含5个整数,依次为\(x_0,a,b,c,d\)描述小H采用的随机数生成算法所需的随机种子。
第2行包含三个整数\(N,M,Q\)表示小H希望生成一个1到\(N \times M\) 的排列来填入她\(N\)行\(M\)列的棋盘,并且小H在初始的\(N \times M\)次交换操作后,又进行了\(Q\)次额外的交换操作。
接下来\(Q\)行,第\(i\)行包含两个整数\(u_i,v_i\),表示第\(i\)次额外交换操作将交换\(T_{u_i},T_{v_i}\)的值。

输出格式

输出一行,包含\(N+M-1\)个由空格隔开的正整数,表示可以得到的字典序最小的路径序列。

输入输出样例

输入
1 3 5 1 71
3 4 3
1 7
9 9
4 9
输出
1 2 6 8 9 12

数据范围

[NOI2014]随机数生成器 题解

题解内容

终于把题目写完了,markdown写了半个多小时
前面blablabla讲了一大堆废话
一句话讲,就是教你怎么生成一个随机函数,代码直接按照它的做法写下来,注意数据范围,要加\(long long\)不然会直接RE就像我一样:
[NOI2014]随机数生成器 题解
炸\(int\)了,所以加上\(long long\)强制转换,就可以了~~~~~~

    scanf("%d%d%d%d%d",&x[0],&a,&b,&c,&d);
	scanf("%d%d%d",&n,&m,&q);
	int s=n*m;
	int i,j,tmp,tmps;
    for(i=1;i<=s+1;i++)
        x[i]=((long long)a*x[i-1]*x[i-1]+(long long)b*x[i-1]+c)%d;
    for(i=1;i<=s;i++) t[i]=i;//注意这里的(longlong),不加会炸掉的
    for(i=1;i<=s;i++){
    	tmps=x[i]%i+1;
    	tmp=t[i];
    	t[i]=t[tmps];
		t[tmps]=tmp;
	}
    for(i=1;i<=q;i++){
    	scanf("%d%d",&a,&b);
    	tmp=t[a];
    	t[a]=t[b];
    	t[b]=tmp;
	}

重头戏在后面,明显是一道贪心,优先考虑1,然后是2,明显最外层的循环是:

for(int i=1;i<=n*m;i++)

当然,register加不加都可以的
然后就是空间上了,这题的空间开两个\(5000\times5000\)的数组是足够的,但是,建议使用一维数组,同时,如果需要三个这样的数组,可以把另一个不需要的数组拿来用,记得清空。

我的做法是先开一个数组来记录总每个数字的编码,位置编码如下:

1 2 3 4
5 6 7 8
9 10 11 12

用数组\(x_i\)来表示第\(i\)个数的位置,然后建立两个数组down和up,如果取了一个数,就在上面做标记。比如说当6号位被取之后,那么9号位、3号位、4号位就不能取了,其中,\(up_i,down_i\)表示第\(i\)列可行的区间即可。每次枚举的时候就可以判定,如果可以,就更新,如果不可以就直接跳过。
这道题目需要很多的细节,所以,经过反复调试,终于,推出了AC代码

#include<cstdio>
#include<cmath>
#include<iostream>
#define maxn 5000*5000+39
using namespace std;
int t[maxn],x[maxn];
int up[5039],down[5039];
int a,b,c,d,n,m,q;
int main(){
	scanf("%d%d%d%d%d",&x[0],&a,&b,&c,&d);
	scanf("%d%d%d",&n,&m,&q);
	int s=n*m;
	int i,j,tmp,tmps;
    for(i=1;i<=s+1;i++)
        x[i]=((long long)a*x[i-1]*x[i-1]+(long long)b*x[i-1]+c)%d;
    for(i=1;i<=s;i++) t[i]=i;
    for(i=1;i<=s;i++){
    	tmps=x[i]%i+1;
    	tmp=t[i];
    	t[i]=t[tmps];
		t[tmps]=tmp;
	}
    for(i=1;i<=q;i++){
    	scanf("%d%d",&a,&b);
    	tmp=t[a];
    	t[a]=t[b];
    	t[b]=tmp;
	}//Insize t
	for(i=1;i<=s;i++)
	    x[t[i]]=i;
	for(i=1;i<=m;i++)
	    up[i]=0,down[i]=n+1;//Insize up and down
	int line,row;
	for(i=1;i<=s;i++){
		line=ceil(1.0*x[i]/m);
		if(x[i]%m==0)
		    row=m;
		else row=x[i]%m;//line  行 row 列 
		if(up[row]<line&&line<down[row]){
			printf("%d ",i);
			for(j=1;j<=row-1;j++)
			    down[j]=min(down[j],line+1);
			for(j=row+1;j<=m;j++)
			    up[j]=max(line-1,up[j]);
		}
	}
	return 0;
}
上一篇:[模板]fhqTreap


下一篇:[NOI2014] 购票