此游戏副本名为PAT甲,相信有不少的玩家都知道此副本,并有部分玩家通关过此副本。此副本能让初级玩家获得丰厚的经验值,甚至可能获得进阶的关键钥匙。本玩家经历了15天终于刷完了此副本,留下副本攻略以供其余玩家参考。
此副本大体分为两个部分,其一是规律题,也即是只要掌握了题怪的运动规律,则可以不掉血也能获得分值,另一是技巧题,这个就得靠历史的打怪经验或是一定的天资了。
规律题分为7种:
技巧题分为3种:
规律题是按照固有的框架搭建的,因此是有规律可循的,也是有固定的模板可供破解的,只要玩家记住固定的心法,便可破之。
STL也称标准模板库,一般的小题,仅仅需要玩家在新手村点亮的技能即可,稍微有点难度的题,也仅需要玩家组合施放技能的能力。一般的小题类似如下:“1006 Sign In and Sign Out”,而有点难度的题也莫过于“1014 Waiting in Line”,攻破他们首要的是了解Vector,Map,Pair,Set,Queue,Stack这些基本容器的用法。
容器 | 用途 |
---|---|
Vector | 变长数组,如遇到需要大数组但直接用数组定义被警告时,可使用。 |
Map | 需要有映射关系时可使用,如:一个人的身份ID与这个人的整个档案进行的直接关联,Map也分为两种:map,unordered_map。map带有自动排序功效,unordered_map则不具其效能,但相应的速度上会快点,若超时时,可尝试更改为unordered_map。 |
Pair | 也称为键值对,若只想利用键值对,但并不需要Map的hash功能可尝试。一般可和Set, Vector等容器组合使用,Set |
Set | 过滤容器,自动帮助你过滤掉相同的元素,方便于去重,Set也分为两种:set, unordered_set,若不需要排序,用unordered_set可提高速度 |
Queue | 队列容器,先入先出的特性使得这武器通常可用于:1. BFS,2. 一批数据经过多道相同工序加工(1056 Mice and Rice) |
Stack | 栈,先入后出的特性,可直接使用此武器的场景不多,但此特性确是DFS,递归算法等的基础,了解即可 |
STL类型的题型分为这样几种:排队题,统计题,模拟题。
排队题和统计题,需要一定的面向对象的思维,即通过struct构造体构造出对象,如 1014 Waiting in Line
struct window
{
int endtime; //结束时间
int capacity; //容量
queue<int> cust; //客户队列
};
此结构体为构造出的银行柜台的对象,构造出对象更能方便玩家思考时按照立体思维模式,而不需要先将思维转换成程序语言再从程序语言整理出题意中种种复杂的关系。
模拟题需要的是对相关概念的理解,如 1051 Pop Sequence 是对栈的一种模拟,只要清楚栈的定义,知道栈是先入后出,能用程序模拟出入栈的操作即可。
STL主要的攻破点是在于如下几点:
前两点只要能读懂题,知道各个容器的特点就可以了,第三点修改对应容器的排序模式,针对不同的容器,修改排序模式的方式有所不同。
数组,Vector,Dequeue:
#include
bool cmp(int a, int b){
return a > b;
}
int main(){
int arr[] = {3,1,2,5,4};
sort(arr, arr+5, cmp);
}
Set(内有红黑树,会在插入时自动排序):
struct Node {
value;
bool operator < const(Node& a) const{
return value < a.value;
}
}
int main(){
set<Node> s;
s.insert(Node{ 0 });
s.insert(Node{ 1 });
}
Priority_queue:
priority_queue<int> q;
priority_queue<int, vector<int>, less<int>> q;
//vector承载底层数据结构堆的容器。
//less表示数字大的优先级大。
//greater表示数字小的优先级小。
struct fruit{
string name;
int price;
friend bool operator < (fruit f1, fruit f2){
//价格高的优先级高
return f1.price < f2.price;
//价格低的优先级高
//return f1.price > f2.price;
}
}
int main(){
priority_queue<fruit> q;
}
struct cmp{
bool operator() (fruit f1, fruit f2){
return f1.price > f2.price;
}
}
int main(){
priority_queue<fruit, vector<fruit>, cmp> q;
}
排序这类题,分为4类,模板分别如下:
const int maxN = 1000010;
int arr[maxN];
void insertSort(){
for(int i=1; i<maxN; i++){
int j=i;
int tmp = arr[i];
while(j > 0 && tmp > arr[j]){
arr[j] = arr[j-1];
j--;
}
arr[j] = tmp;
}
}
const int maxN = 100010;
int arr[maxN];
void merge(int L1, int R1, int L2, int R2){
int i = L1;
int j = L2;
int idx = 0;
int tmp[maxN];
while(i<=R1 && j<=R2){
if(arr[i]<arr[j]){
tmp[idx++] = arr[i];
}else{
tmp[idx++] = arr[j];
}
}
while(i<=R1){
tmp[idx++] = arr[i];
}
while(j<=R2){
tmp[idx++] = arr[j];
}
for(int k=0; k<idx; k++){
arr[L1+k] = tmp[k];
}
}
void mergeSort(){
for(int step=2; step/2<maxN; step*=2){
for(int i=0; i<N; i+=step){
int mid = i+step/2-1;
if(mid+1<N){
merge(i, mid, mid+1, min(i+step-1, maxN-1));
}
}
}
}
const int maxN = 100010;
int arr[maxN+1];
void downAdjust(int low, int high){
int i = low;
int j = 2*i+1;
while(j <= high){
if(j+1 <= high && arr[j+1] > arr[j]){
j+=1;
}
if(arr[i] < arr[j]){
swap(arr[i], arr[j]);
i = j;
j = 2*i+1;
}else{
break;
}
}
}
void createHeap(){
for(int i=maxN/2; i>=1; i--){
downAdjust(i, maxN);
}
}
void sortHeap(){
createHeap();
for(int i=N; i>=1; i--){
swap(arr[i], arr[1]);
downAdjust(1, i-1);
}
}
表排序是一个方法,如,重量级数组A[] = {book1, book2, book3},每一个book包含了10GB的内容,如果直接进行排序,内存中移动book会是相当慢的过程。此时表外排序将是好的选择,将book1的地址索引0存到B[0]中,book2的地址索引1存到B[1]中,book3的地址索引2存到B[2]中,排序B时需要读取数据才通过索引读取A的数据即可。
相关题目 1067 Sort with Swap(0, i)
表排序
1067 Sort with Swap解析
二叉树是指树中节点的度不大于2的有序树。唯一 确定一个二叉树的方式有这样4种:
不能唯一 确定一个二叉树的方式:
能否确定二叉树的关键在于
这些建二叉树的关键因素怎么和题提供的线索绑定起来,可以看看是否有下表的关系。记:先序序列pre[],中序序列in[],后序序列post[]。先序序列的开始索引和结束索引为preLeft,preRight。中序序列的开始索引和结束索引为inLeft,inRight。后序序列的开始索引和结束索引为postLeft,postRight。完全二叉树的开始索引和结束索引为CBTLeft,CBTRight。
线索 | 根节点 | 左子树 | 右子树 |
---|---|---|---|
先序+中序(1138 Postorder Traversal) | pre[preLeft] | 求与pre[preLeft]值相同的中序序列索引rootIdx,左子树构造节点:inLeft~rootIdx | 求与pre[preLeft]值相同的中序序列索引rootIdx,右子树构造节点:rootIdx + 1 ~ inRight |
后序+中序 (1020 Tree Traversals) | post[postRight] | 求与post[postRight]值相同的中序序列索引rootIdx,左子树构造节点:inLeft~rootIdx | 求与post[postRight]值相同的中序序列索引rootIdx,右子树构造节点:rootIdx + 1 ~ inRight |
父节点索引:左孩子索引,右孩子索引 | 度为0的节点为整个树根节点 | 根据父子关系可得左子树 | 根据父子关系可得右子树 |
入栈出栈序列:入栈序列即先序序列,出栈序列即中序序列(1086 Tree Traversals Again) | 同先序+中序 | 同先序+中序 | 同先序+中序 |
完全二叉树+二叉搜索树(1064 Complete Binary Search Tree ) | 构造树节点:N = GBTRight-GBTLeft+1,构造树的层数:L = log(N+1)/log(2),叶子节点数:leave = N - (pow(2, L) - 1),根节点:root = GBTLeft+(pow(2, L-1)-1)+min(leave, (int)pow(2, L-1)) | 左子树为GBTLeft~root-1 | 右子树为root+1~GBTRight |
二叉树的存储方式分为2种:
链表式存储是构造一般二叉树的经典方式,只要是二叉树都可用链表的方式进行存储。
struct Node{
int data;
Node* lchild;//左子树
Node* rchild;//右子树
}
Node* createNode(int d){ //创建节点
Node nd = new Node;
nd->lchild = NULL;
nd->rchild = NULL;
nd->data = data;
return nd;
}
数组式存储用于完全二叉树层次遍历的记录,root为根在数组中的索引,2*root为其左孩子在数组中的索引,2*root+1为其右孩子在数组中的索引。注:此数组的开始节点的索引为1。
二叉树继续细分,则可分为完全二叉树,平衡二叉树,红黑树,其某些特性可作为其判断的依据。
特殊树 | 特性 |
---|---|
完全二叉树(1110 Complete Binary Tree) | 对树进行层次遍历时,对每个节点标号,从root=1号开始,左孩子标号2*root,右孩子标号2*root+1,则最后一个节点的标号必然等于构成树的节点总数 |
平衡二叉树(1066 Root of AVL Tree) | 定义:平衡系数=左子树的高度-右子树的高度,若添加完一个节点后根节点的平衡系数为2,其左子树的平衡系数为1,则需要右旋,若其左子树的平衡系数为-1,则需要先将其左子树左旋,再将根节点右旋。若根节点的平衡系数为-2,其右子树的平衡系数为-1,则需根节点左旋,若其右子树的平衡系数为1,则需先将其右子树右旋,再将根节点左旋。可简单记为:若LL,则R旋,若LR,则LR旋,若RR,则L旋,若RL,则RL旋 |
红黑树(1135 Is It A Red-Black Tree) | 根节点必为黑色。若某节点为红色,则其孩子节点为黑色。从某个节点出发,到其所有子节点的路径上的黑色节点数量相同。 |
DFS又可谓之不撞南墙不回头,这有标准模板,直接套模板就行。
void dfs(int 下一步){
if(撞到了南墙)
return;//回头
dfs(下一步+1);
}
int main(){
dfs(第一步);
}
BFS又谓之排队大法,这还是有标准模板,一起套模板吧。
void bfs(int s){
queue<int> q;
q.push(s);
while(!q.empty()){
取出队首节点top;
访问队首节点top;
将队首元素出队;
将top的下一层节点中未曾入队的节点全部入队,并设置为已入队;
}
}
DFS,BFS是作为探究关系的工具所存在,用这两个工具可以理清各个关系与结构。
关系类别 | 功用 |
---|---|
二叉树 | 遍历节点,获得层次遍历序列。唯一确定二叉树必要条件+遍历可构造二叉树 |
联通图 | 判断是否处于同一联通图:若从一个节点出发可以遍历到所有节点(1013 Battle Over Cities) |
图 | 遍历所有节点。判断欧拉图,半欧拉图,半欧拉环路,哈密尔顿环,(1126 Eulerian Path,1122 Hamiltonian Cycle)商旅问题(1150 Travelling Salesman Problem),Djkstra的最短路径辅助判断 |
DFS,BFS一般是可以互相转换使用的,但使用时仍需注意些许不同的地方。
DFS的函数参数能够传递层次,传递下一个状态,传递递推的信息。如若需要用DFS遍历二叉树,想要获得其高度。便可将高度信息作为函数的一个参数传递下去。
struct Node{
int data;
Node* lchild;
Node* rchild;
}
void dfs(Node* root, int height){
if(root==NULL){
return;
}
dfs(root->lchild, height+1);
dfs(root->rchild, height+1);
}
若需要用BFS,则不同于递归函数的参数,此时可以用节点,结构体传输信息。
struct Node{
int data;
Node* lchild;
Node* rchild;
int level;//层次
}
void bfs(Node* root){
queue<Node*> q;
root->level = 1;
q.push(root);
while(!q.empty()){
Node* nd = q.front();
q.pop();
if(nd->lchild!=NULL){
nd->lchild->level = nd->level+1;
q.push(nd->lchild);
}
if(nd->rchild!=NULL){
nd->rchild->level = nd->level+1;
q.push(nd->rchild);
}
}
}
并查集和拓扑结构也是描述多节点关联关系的结构,并查集的作用为归集,将有关系的节点归集到一起。拓扑结构的作用为发展,将各个节点的先后关系展现出来,用于判断一个给定的图是否是有向无环图。
并查集(1114 Family Property,1118 Birds in Forest)对应的模板代码:
const int maxN = 10010;
int father[maxN];
void initFather(){
for(int i=0; i<maxN; i++){
father[i] = i;
}
}
int findFather(int x){
if(father[x]==x){
return father[x];
}else{
int F = findFather(father[x]);
father[x] = F;
return F;
}
}
void unionGroup(int x, int y){
if(findFather(x)!=findFather(y)){
father[findFather(x)] = findFather(y);
}
}
拓扑排序(1146 Topological Order)的代码模板
const int maxN = 10010;
vector<int> G[maxN];
int n, m, inDegree[maxN];
bool topologicalSort(){
int num = 0;
queue<int> q;
for(int i=0; i<n; i++){
if(inDegree[i]==0){
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
for(int i=0; i<G[u].size(); i++){
int v = G[u][i];
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
}
G[u].clear();
num++;
}
if(num == n) return true;
else return false;
}
Dijkstra是用来解决 单源最短路径问题,即给定图G和起点S,通过算法得到S到达其他每个顶点的最短距离。一般需要Dijkstra的场景都需要与DFS搭配使用才能解决问题。Dijkstra可求得最短路径,和对应的前置路径节点集,一般最短路径不只有一条,这时需要其他条件从这些相同最短路径中筛选出最合适的那一条,这时就需要DFS了。
图结构的构造有两种方式,一种为邻接矩阵,一种为邻接表。因为邻接矩阵需要直接定义二维数组,若节点个数不是很多的情况下可直接用邻接矩阵,若节点数目过多,则最好采用邻接表的方式定义图。
//邻接矩阵
G[N][N];
//邻接表
vector<int> Adj[N];
//存放终点编号和边权的邻接表
struct Node{
int v; //边的终点编号
int w; //边权
}
vector<Node> Adj[N];
Dijkstra最短路径+DFS(1018 Public Bike Management)模板代码
const int maxN = 100;
const int INF = 99999999;
int G[maxN][maxN];
bool visited[maxN];
int d[maxN];
int start; //开始
int dest; //终止
int N; //总点数
vector<int> pre[maxN];
vector<int> temppath;
vector<int> path;
void dfs(int start){
temppath.push_back(start);
if(start==dest){
for(int i=temppath.size()-1; i>=0; i--){
//根据每一条路径的处理内容
}
temppath.pop_back();
return;
}
for(int i=0; i<pre[start].size(); i++){
dfs(pre[start][i]);
}
temppath.pop_back();
}
int main(){
fill(d, d+N, INF);//初始化
d[start]=0;
for(int i=0; i<N; i++){
int mind=INF;
int u=-1;
for(int j=0; j<N; j++){
if(!visited[j] && d[j]<mind){
mind=d[j];
u=j;
}
}
if(u==-1){
break;
}
visited[u]=true;
for(int j=0; j<N; j++){
if(!visited[j] && G[u][j]!=INF){
if(d[u]+G[u][j]<d[j]){
d[j]=d[u]+G[u][j];
pre[j].clear();
pre[j].push_back(u);
}else if(d[u]+G[u][j]==d[j]){
pre[j].push_back(u);
}
}
}
}
dfs(start);
}
判断是否是大数运算的条件是看题目中提示的输入数字的范围是否大于10^18。大数运算分为两种,一种是整数型大数的运算,一种是分数型大数的运算。
整数型大数的处理方式是以字符数组的形式读取数据转换为整型数组,然后分别对数字的每一位进行运算。
struct bign{
int d[1000];
int len;
bign(){
memset(d, 0, sizeof(d));
len=0;
}
}
/**
将读入的字符数组转换为大数结构体
**/
bign change(char str[]){
bign a;
a.len = strlen(str);
for(int i=0; i<a.len; i++){
a.d[i] = str[a.len-i-1] - '0';
}
return a;
}
/**
大数加法
**/
bign add(bign a, bign b){
bign c;
int carry=0;//进位
for(int i=0; i<a.len||i<b.len; i++){
int temp = a.d[i]+b.d[i]+carry;
c.d[c.len++] = temp%10;
carry = temp/10;
}
if(carry!=0){
c.d[c.len++] = carry;
}
}
/**
大数减法
**/
bign sub(bign a, bign b){
bign c;
for(int i=0; i<a.len || i<b.len; i++){
if(a.d[i]<b.d[i]){
a.d[i+1]--;
a.d[i]+=10;
}
c.d[c.len++] = a.d[i]-b.d[i];
}
//去掉高位的0,至少保留一位最低位
while(c.len-1>=1 && c.d[c.len-1] == 0){
c.len--;
}
return c;
}
/**
大数乘法
**/
bign multi(bign a, bign b){
bign c;
int carry = 0;
for(int i=0; i<a.len; i++){
int temp = a.d[i]*b+carry;
c.d[c.len++]=temp%10;
carry=temp/10;
}
while(carry!=0){//乘法的进位不止一位
c.d[c.len++]=carry%10;
carry/=10;
}
return c;
}
/**
大数除法
**/
bign divide(bign a, bign b, int& r){
bign c;
c.len = a.len;
for (int i=a.len-1; i>=0; i--){
r = r*10+a.d[i];
if(r<b){
c.d[i]=0;
}else{
c.d[i] = r/b;
r = r%b;
}
}
while(c.len-1>=1 && c.d[c.len-1]==0){
c.len--;
}
return c;
}
分数型大数(1088 Rational Arithmetic)的处理方式可看作分别对分子和分母进行处理。
struct Fraction{
int up, down;
};
/**
求最大公约数
**/
int gcd(int a, int b){
if(b==0){
return a;
}
return gcd(b, a%b);
}
/**
分数化简
**/
Fraction reduction(Fraction result){
if(result.down<0){
result.up = -result.up;
result.down = -result.down;
}
if(result.up==0){
result.down = 1;
}else{
int d = gcd(abs(result.up), abs(result.down));
result.up/=d;
result.down/=d;
}
return result;
}
/**
分数加法
**/
Fraction add(Fraction f1, Fraction f2){
Fraction result;
result.up = f1.up*f2.down+f2.up*f1.down;
result.down = f1.down*f2.down;
return reduction(result);
}
/**
分数减法
**/
Fraction minu(Fraction f1, Fraction f2){
Fraction result;
result.up = f1.up*f2.down-f2.up*f1.down;
result.down = f1.down*f2.down;
return reduction(result);
}
/**
分数乘法
**/
Fraction multi(Fraction f1, Fraction f2){
Fraction result;
result.up = f1.up*f2.up;
result.down = f1.down*f2.down;
return reduction(result);
}
/**
分数除法
**/
Fraction divide(Fraction f1, Fraction f2){
Fraction result;
result.up = f1.up*f2.down;
result.down = f1.down*f2.up;
return reduction(result);
}
string的构造方法有多种,如下介绍常见2种。
string str;
cin >> str;
char c[100];
scanf("%s", c);
string str = string(c);
遍历方法介绍如下常见2种。
string str = "abc";
for(int i=0; i<str.size(); i++){
printf("%c", str[i]);
}
string str = "abc";
for(string::iterator it=str.begin(); it!=str.end(); it++){
printf("%c", *it);
}
删除元素的方法可分为如下2种。
string str = "abcdefg";
str.erase(str.begin()+2, str.end()-1);
cout << str << endl;
输出结果:
abg
string str = "abcdefg";
str.erase(3, 2);
cout << str << endl;
输出结果:
abcfg
查找分为查找字符串,或者查找某个字符。
string str = "abcdeft";
int cPosition = str.find_first_of('c');
string str = "abcdefg";
int idx = str.find("cde");
int idx2 = str.find("cde", 2);//从2位置开始匹配cde子串
通常此转换用于简单的大数字计算场景。
//string 转 int
string str = "111";
int a = stoi(str);
//int 转 string
str = to_string(a);
//char[] 转 int
string str = "111";
char c[5] = "111";
int b = atoi(c);
b = atoi(str.c_str());
若需要将string整体全转为大写或者小写则可按如下方法转换。
string str = "abcABC";
//转换大写
transform(str.begin(), str.end(), str.begin(), ::toupper);
//转换小写
transform(str.begin(), str.end(), str.begin(), ::tolower);
数组是用于存放数据的基本容器,下面介绍3种此容器可承载的数据。
数据类型 | 简要描述 | 题型 |
---|---|---|
数量 | 数组的索引具有唯一性,一个能标记ID的物体也具有唯一性,物体的唯一性与索引唯一性便可一一对应,而对应索引数组存放的数据便可体现物体出现的数量 | 1048 Find Coins :硬币的面值具有唯一性,可作为数组的索引,不同面值硬币出现的次数可存放在对应数组的索引下。1054 The Dominant Color :不同的颜色具有唯一性,可作为数组的索引,不同颜色出现的次数可存放在对应数组的索引下。1057 Stack :题目提供的数字是有上限的,将其数字与数组索引一一对应,数字出现的次数存入对应数组索引下。若已出现N个数,中间数则为第N/2次出现的,用树状数组求和便可方便求得了。1092 To Buy or Not to Buy :此题与 1048 Find Coins 相同,只是准备时需要将[0-9,a-z,A-Z]转换为0-36即可 |
地址 | 简单链表需要存放3个数据,本节点地址,本节点值,下一节点地址,地址具有唯一性,与数组索引唯一性关联。 | 1074 Reversing Linked List 可用2个数组表示链表,数组A的索引即本节点地址Addr,数组A的索引A[Addr]下存放对应本节点值,数组B的索引B[Addr]下存放下一节点地址。1097 Deduplication on a Linked List 每个节点的地址唯一性与数组索引一一关联,数组索引下存放每个节点结构体{本节点地址,本节点数据,下一节点地址}。 1133 Splitting A Linked List 同 1097 Deduplication on a Linked List 。1145 Hashing - Average Search Time Hash二次探查法,二次探查法【(数值+step*step)%hash数组大小】获得存储值的数组地址 |
签到 | 数组索引唯一性可表示唯一物体,数组索引下的值对应此物体是否出现 | 1121 Damn Single 数组索引表示人的ID,若此人A出现则更新签到数组,更新此人的情侣B的ID的签到索引下值为已签到。遍历到情侣B的人时,若签到下值为已到,说明情侣A已到。1149 Dangerous Goods Packaging 数组的索引与危险物ID一一对应,数组的值为危险物是否出现。 |
记忆 | 数组的索引具有单调递增的特性,与时间轴的延展有相同特性,数组的idx的状态可看作当前状态,idx-1可看作过去状态,idx+1可看作未来态 | 1093 Count PAT’s 数组每个索引下的值表示此位置即之前出现了多少次P,只与前一位置对应P的数量相关。1101 Quick Sort 从左向右遍历,数组leftMax记录到对应位置的最大值,与前一位置的最大值相关,从右向左遍历,数组rightMin记录到对应位置的最小值,与前一位置的最小值相关。 |
树状数组求和模板:
#define lowbit(i) ((i) & (-i))
const int maxN = 100010;
int c[maxN];
int getSum(int x){
int sum = 0;
for(int i=x; i>=1; i-=lowbit(i)){
sum += c[i];
}
return sum;
}
void update(int x, int v){
for(int i=x; i<maxN; i+=lowbit(i)){
c[i]+=v;
}
}
规律题破解攻略解析到此就结束了,想必各位玩家已早已洞悉一切,规律题之所以含有规律二字,便是因为设计者设计题时也是有所设计的上界和下界的,也是有所参考框架的。只要掌握了框架,直接套用便可攻破了。
To be continue…