【题目】 【思路】
这道题目要我们将序列调整为不下降序列,那肯定要对相邻元素进行考虑,使任意一对相邻元素,后者都不小于前者,即观察两两元素之差。
利用贪心的思想来做,对于给定的序列,观察每一对相邻的元素后者是否大于前者:
如果大于等于,满足不下降就不需要调整;如果小于,那么需要将前者减少,或者将后者增大,操作次数为两元素之差。那到底是调整前者还是调整后者?我认为如果对序列从左向右调整,那么应该尽可能让前者减少,这样可以让右边的序列部分有更多调整空间;反之对序列从右向左调整,应该先考虑让后者增大,这样可以给左边的序列让出更大的调整空间。
例如:对于序列: 2 , 6 , 2 , 5 , 2 2, 6, 2, 5, 2 2,6,2,5,2,假设从右向左调整,首先对于最后一对相邻元素 5 , 2 5, 2 5,2 计算出后者减前者的差为-3,那么需要进行调整,此时当然应该调整后者2,将其操作三次加到5,而不应该对前者5进行调整,虽然也是调整三次,但是变成2了会导致前面的序列调整空间变小。那么调整完后序列变为 2 , 6 , 2 , 5 , 5 2, 6, 2, 5, 5 2,6,2,5,5 。
上面说的是尽可能先调整什么,那么什么时候可以按照我们的想法调整,什么时候不得不调整另一个元素,也就是对于上面的例子最后一对相邻元素 5 , 2 5, 2 5,2 来说,一定可以将后者2增大为5吗?会不会出现只能调整前者5的情况?
对于调整后的序列 2 , 6 , 2 , 5 , 5 2, 6, 2, 5, 5 2,6,2,5,5 ,此时考虑倒数第二对相邻元素 2 , 5 2, 5 2,5,后者比前者大,无需调整;
考虑倒数第三对相邻元素 6 , 2 6, 2 6,2,后者大于前者,按照想法,期望将后者2调整为6,但是如果这样调整了,序列将变为 2 , 6 , 6 , 5 , 5 2, 6, 6, 5, 5 2,6,6,5,5,破坏了已经调整好的右子序列。
这时就找到了调整的规律,不能只考虑当前对相邻元素的差,还需要考虑后一对元素的差,那么对于序列 6 , 2 , 5 6, 2, 5 6,2,5 来说,令left表示前一对元素差,right表示后一对元素差,那么 l e f t = − 4 , r i g h t = 3 left = -4, right = 3 left=−4,right=3; 因为 ∣ l e f t ∣ > r i g h t |left| > right ∣left∣>right,所以此时必须调整前者,否则会破坏调整好的右子序列。
【代码描述】
有了上述的过程,从右向左调整的过程就可以写出代码了(其中temp2存放序列,right初值设为很大的数INF,因为最右边的肯定可以无限增大,所有最后一对相邻元素一定可以调整后者):
int right_solve(){ // 从右向左计算 int ans = 0; int right = INF, left; for(int i=n-2; i>=0; i--){ left = temp2[i+1] - temp2[i]; if(left >= 0){ // 满足升序关系,更新right right = left; } else{ if(left+right >= 0){ // 尽量让右边的变大,给左边留下空间 temp2[i+1] -= left; } else{ // 只能让左边的减小 temp2[i] += left; } // 调整好后,这一对相邻元素的差为0 right = 0; // 累加操作次数 ans -= left; } } return ans; }做到这里我发现对于序列 7 , 7 , 7 , 7 , 6 , 5 7, 7, 7, 7, 6, 5 7,7,7,7,6,5,按照上述从右向左的调整过程,会变为 6 , 6 , 6 , 6 , 6 , 6 6, 6, 6, 6, 6, 6 6,6,6,6,6,6,调整次数为5,但是如果从左向右调整,会变为 7 , 7 , 7 , 7 , 7 , 7 7, 7, 7, 7, 7, 7 7,7,7,7,7,7,调整次数为3,显然应该取两者更小值。
【完整代码】
#include<iostream> using namespace std; const int maxn = 5010; const int INF = 0x3fffffff; int num[maxn], temp1[maxn], temp2[maxn]; int n; void print(int* A){ // 打印出num数组内容 for(int i=0; i<n; i++){ printf("%d", A[i]); if(i != n-1) printf(" "); else printf("\n"); } } int left_solve(){ // 从左向右计算 int ans = 0; // 操作次数 int left = INF, right; for(int i=0; i<n-1; i++){ right = temp1[i+1] - temp1[i]; if(right >= 0){ // 满足升序关系,更新left left = right; } else{ if(left+right >= 0){ // 尽量让左边的减小,给右边留下空间 temp1[i] += right; left = 0; } else{ // 只能更新右边的 temp1[i+1] -= right; } left = 0; ans -= right; // 操作次数应加上right的绝对值 } } return ans; } int right_solve(){ // 从右向左计算 int ans = 0; int right = INF, left; for(int i=n-2; i>=0; i--){ left = temp2[i+1] - temp2[i]; if(left >= 0){ // 满足升序关系,更新right right = left; } else{ if(left+right >= 0){ // 尽量让右边的变大,给左边留下空间 temp2[i+1] -= left; } else{ // 只能让左边的减小 temp2[i] += left; } right = 0; ans -= left; } } return ans; } int main(){ scanf("%d", &n); for(int i=0; i<n; i++){ scanf("%d", &num[i]); temp1[i] = num[i]; temp2[i] = num[i]; } // print(num); int lnum = left_solve(); // printf("lnum: %d\n", lnum); // print(temp1); int rnum = right_solve(); // printf("rnum: %d\n", rnum); // print(temp2); if(lnum < rnum) printf("%d", lnum); else printf("%d", rnum); return 0; }【题目】 【思路】 我这里用到的是动态规划的思想,假设 d p [ x ] [ n ] dp[x][n] dp[x][n]数组的含义为数n可以用1,2,3…x组成的和的形式的个数。 比如 d p [ 2 ] [ 5 ] dp[2][5] dp[2][5]就表示5可以拆成1,2组成的和的形式个数,那么有:
1 + 1 + 1 + 1 + 1 1+1+1+1+1 1+1+1+1+1 1 + 1 + 1 + 2 1+1+1+2 1+1+1+2 1 + 2 + 2 1+2+2 1+2+2那么很容易想到 d p [ 2 ] [ 5 ] dp[2][5] dp[2][5]会和 d p [ 1 ] [ 5 ] dp[1][5] dp[1][5]有关,因为:
5拆成用1,2表示的形式数= (5拆成用1表示的形式数) + (5拆成有2的和表示的形式数)
因此: d p [ 2 ] [ 5 ] = d p [ 1 ] [ 5 ] dp[2][5] = dp[1][5] dp[2][5]=dp[1][5] + 一定有2的和的组合;
那后者怎么计算呢? 我们可以想到5中一定要有2,那不就是3的组合数每个加2:
1 + 1 + 1 1+1+1 1+1+1 --> 1 + 1 + 1 + 2 1+1+1+2 1+1+1+2 1 + 2 1+2 1+2 --> 1 + 2 + 2 1+2+2 1+2+2即dp[2][3],于是找到了转移方程 d p [ 2 ] [ 5 ] = d p [ 1 ] [ 5 ] + d p [ 2 ] [ 3 ] dp[2][5] = dp[1][5] + dp[2][3] dp[2][5]=dp[1][5]+dp[2][3], 写成一般项为: d p [ x ] [ n ] = d p [ x − 1 ] [ n ] + d p [ x ] [ n − x ] dp[x][n] = dp[x-1][n] + dp[x][n-x] dp[x][n]=dp[x−1][n]+dp[x][n−x]
但是上式是有条件的,因为比如dp[3][2],应该是数2由1构成的和的组合数,加上2由包含3的和的组合数,但是因为2比3小,所以没有由3构成的组合数,那么对于n < x的情况,后一项是0,变为了: d p [ x ] [ n ] = d p [ x − 1 ] [ n ] dp[x][n] = dp[x-1][n] dp[x][n]=dp[x−1][n]
【初始值的确定】
容易想到对于一张dp[x][n]的二维表来说,我们可以初始化dp[1][n] = 1,因为任何数x拆成只有1的组合都是x个1相加,也可以初始化dp[0][n] = 0,任何书都没法拆成只有0的和。
-01234500000001应该为11111123初始化后可以看到按照公式,dp[1][0]应该填1(才能得出dp[1][1] = dp[0][1] + dp[1][0]),此时就可以按照推出的公式来了:
// 求dp[x][n] if(n >= x){ dp[x][n] = dp[x-1][n] + dp[x][n-x]; } else{ dp[x][n] = dp[x-1][n]; }那么这个表应该为:
-0123450000000111111121122333112345可以发现dp[x][0]都应该为1,可以自行理解一下。那么dp[3][n]就是所求答案了。 观察表中的规律,方块等于两圆圈和。
【源代码】
#include<iostream> #include<cstring> using namespace std; const int maxn = 40000; // dp[x][n]表示数n可以拆成用1,2...x的和组成形式的个数 long long dp[4][maxn]; long long solve(int x){ // 初始化 for(int i=1; i<=x; i++){ dp[0][i] = 0; } for(int i=1; i<=3; i++){ dp[i][0] = 1; } for(int i=1; i<=3; i++){ for(int j=1; j<=x; j++){ // 数j拆成的组合等于数j-1拆成的组合数(每一种组合后加1就是j了) dp[i][j] = dp[i-1][j]; if(j >= i){ // 新添加包含i的组合,那么等于数j-i拆成的组合数 (每一种组合后加i就是j了) dp[i][j] += dp[i][j-i]; } } } return dp[3][x]; } int main(){ int n; while(scanf("%d", &n) != EOF){ long long ans = solve(n); printf("%lld\n", ans); } return 0; }【题目】 【解题思路】
这里有两个问题需要考虑,分别是:
如何计算路线条数?如何按要求打印出路线?首先第一个问题:路线条数的计算。
观察蜂窝图可以发现:对于每个数x来说,下一步只能是x+1或者x+2。那能够想到的是,路线条数和起点终点无关,而只和二者之间的差值有关,即 |终点 - 起点| 的值。
上面说到的话反过来看,就是对于终点x,能够到达这里的路线只有两种可能,一种是从x-1过来的,一种是从x-2过来的。那么不就是斐波那契数列嘛, f [ n ] = f [ n − 1 ] + f [ n − 2 ] f[n] = f[n-1] + f[n-2] f[n]=f[n−1]+f[n−2],初始值为 f [ 1 ] = 1 , f [ 2 ] = 2 f[1] = 1, f[2] = 2 f[1]=1,f[2]=2
因为,当二者差值为1时,路线条数只有一条;当二者差值为2时,有两条路线。比如起点为2,终点为3,那么只有一条路线(2,3);起点为2,终点为4,那么有两条路线(2,3,4)和(2,4);
第二个问题,如何按照要求打印出路线?
这显然是一个递归的过程,我定义了一个打印函数:
void print(int cur);cur表示当前加入的数,显然递归边界是cur为终点;将cur加入路线后,可以考虑将cur+1和cur+2加入路线,然后递归调用print(cur+1)或者print(cur+2); 该函数可以写为:
void print(int cur){ if(cur > b) return; // 当前处理的数 if(cur == b){ for(int i=0; i<route.size(); i++){ cout << route[i] << " "; } cout << endl; return ; } for(int i=cur+1; i<=cur+2; i++){ route.push_back(i); print(i); route.pop_back(); } }这样就按照字典序打印出了所有路线: 但是题目只要求输出不同的部分,并且只输出n条路线,因此需要多加一些约束条件,可以发现公共前缀的最后一个元素的特点是:下一个元素和该元素的差不是1了,也就是只要增量是2,就说明到了不一样的地方,该输出了。
【源代码】
#include<iostream> #include<vector> using namespace std; // 路线 vector<int> route; // 起点,终点和需要输出的路径条数 int a, b, n; // 路线条数和起点、终点无关,只和二者之差有关 int solve(int len){ int pre = 1, p = 2; len -= 2; for(int i=1; i<=len; i++){ int temp = p; p = pre+p; pre = temp; } return p; } // 打印字典序前n的路线 int num = 0; void print(int cur){ if(cur > b) return; // 当前处理的数 if(cur == b){ num++; if(num <= n){ bool flag = false; // 不需要输出 if(num == 1) flag = true; // 第一次所有都要输出 for(int i=0; i<route.size(); i++){ if(flag){ cout << route[i] << " "; } else{ if(i!=route.size()-1 && route[i+1] != route[i]+1){ // 后一个数发生了跳越,就要开始输出了 flag = true; cout << "< " << route[i] << " > "; } } } cout << endl; } return ; } for(int i=cur+1; i<=cur+2; i++){ route.push_back(i); print(i); route.pop_back(); } } int main(){ cin>>a>>b>>n; int ans = solve(b-a); cout<<ans<<endl; route.push_back(a); print(a); return 0; }【解题思路】
这是一道搜索题目,用BFS非常合适,因为对每个结点来说,它相对于起点(1, 1, 1)的层数就是Yuki到这个点所需要的时间。
那么就方便了,用一个三维数组matrix存放每个点的情况,全部初始化为0,-1表示墙,matrix[x][y][z] = t表示该结点在t时刻,以及后面的t时间内都不能走。也就是访问到这个结点时,用该结点相对起点的层数和matrix值比较即可。
因此对于每个结点,有四个元素需要保存:
struct node{ int x, y, z; // 坐标 int level; //层数 };【算法描述】 用队列queue来实现BFS,将队首元素出队,判断其是否已经到了门口并且层数小于T(大魔王回来的时间),是则返回该结点的层数即为最少要花的时间;否则判断其上下左右前后的六个结点是否需要访问,是的话更新其层数为当前结点层数加1,否则无操作。
判断某结点是否需要访问(注意越界、入过队列、墙、技能时间范围内):
bool judge(int x, int y, int z, int time){ // 判断坐标(x,y,z)是否可以走 // 越界 if(x<1 || x>l || y<1 || y>w || z<1 || z>h) return false; // 已经入过列 if(inq[x][y][z]) return false; // 如果是墙不能走 if(matrix[x][y][z] == -1) return false; // 如果有天火和磁暴不能走 if(time >= matrix[x][y][z] && time <= 2*matrix[x][y][z] ) return false; return true; }注意在BFS中每个结点仅入队列一次,所以创建一个三维bool数组inq指示某坐标点是否已经入队过。
【源代码】
#include<iostream> #include<queue> using namespace std; const int maxn = 80; int l, w, h, T; // 城堡的长宽高以及卡尔回来所需的时间 struct node{ int x, y, z; // 坐标 int level; //层数 }start, temp; //int time = 0; // 当前时刻,完全可以用层数表示 int matrix[maxn][maxn][maxn]; // -1表示墙,其他表示当前时刻t和后t时刻都不能通过 bool inq[maxn][maxn][maxn]; // 是否入过队列 int X[6] = {-1, 1, 0, 0, 0, 0}; // 增量矩阵 int Y[6] = {0, 0, -1, 1, 0, 0}; int Z[6] = {0, 0, 0, 0, -1, 1}; bool judge(int x, int y, int z, int time){ // 判断坐标(x,y,z)是否可以走 // 越界 if(x<1 || x>l || y<1 || y>w || z<1 || z>h) return false; // 已经入过列 if(inq[x][y][z]) return false; // 如果是墙不能走 if(matrix[x][y][z] == -1) return false; // 如果有天火和磁暴不能走 if(time >= matrix[x][y][z] && time <= 2*matrix[x][y][z] ) return false; return true; } int BFS(int x, int y, int z){ // 起点默认为(1, 1, 1) // 终点为(l, w, h) start.x = x, start.y = y, start.z = z; start.level = 0; queue<node> Q; Q.push(start); inq[x][y][z] = true; while(!Q.empty()){ node top = Q.front(); Q.pop(); if(top.x==l && top.y==w && top.z == h){ // 遇到了终点返回其层数 return top.level; } if(top.level > T){ // 时间到了,大魔王回来了 return -1; } for(int i=0; i<6; i++){ int newX = top.x + X[i]; int newY = top.y + Y[i]; int newZ = top.z + Z[i]; int level = top.level+1; // 到这个结点的时间为层数 if(judge(newX, newY, newZ, level)){ temp.x = newX, temp.y = newY, temp.z = newZ; temp.level = level; Q.push(temp); inq[newX][newY][newZ] = true; } } } } int main(){ scanf("%d%d%d%d", &l, &w, &h, &T); int k, a, b, c, t; scanf("%d", &k); // 城堡初始化 for(int i=1; i<=l; i++){ for(int j=1; j<=w; j++){ for(int k=1; k<=h; k++){ matrix[i][j][k] = 0; } } } for(int i=0; i<k; i++){ scanf("%d%d%d", &a, &b, &c); matrix[a][b][c] = -1; // 墙 } scanf("%d", &k); for(int i=0; i<k; i++){ scanf("%d%d%d", &t, &a, &b); // 天火 for(int j=1; j<=h; j++){ // 注意城堡从(1, 1, 1)开始,不是(0, 0, 0) matrix[a][b][j] = t; // 在t时刻和后t时间内,上下不能走 } } scanf("%d", &k); for(int i=0; i<k; i++){ scanf("%d%d%d%d", &t, &a, &b, &c); // 磁暴 for(int j=0; j<4; j++){ matrix[a+X[j]][b+Y[j]][c] = t; // 在t时刻和后t时间内,前后左右不能走 } } int ans = BFS(1, 1, 1); printf("%d\n", ans); return 0; } /* input: 3 3 2 10 3 1 3 1 2 3 2 1 1 2 2 1 1 1 3 3 1 1 2 3 1 1 output: 5 input:(仅时间减少) 3 3 2 4 3 1 3 1 2 3 2 1 1 2 2 1 1 1 3 3 1 1 2 3 1 1 output: -1 */【解题思路】
看着这是一道拓扑排序的题呀,但是第一个样例中发现有相同的(u, v)组合,处理的时候要注意,我是用bool型邻接矩阵存的。
拓扑排序就是不断的把入度为0的结点加入拓扑序列,这里还要注意满足题目中说的编号越小的人越应该靠前进场,这里我不是很明白,如果是要求输出最小的字典序列的话,那么第二个样例应该输出:2 4 1 3,但样例给的输出是:4 1 2 3;二者都是满足约束的拓扑排序,我的代码是输出最小字典序的拓扑排序,不知道加粗的话如何理解。可能有其他理解方式像之前的蓝桥杯似的,就不纠结了,当复习一下拓扑排序好了。
【源代码】
#include<iostream> #include<queue> #include<vector> using namespace std; const int maxn = 30010; bool G[maxn][maxn]; int inDegree[maxn]; // 每个顶点的入度 int n, m; vector<int> res; // 答案 void topoSort(){ queue<int> q; for(int i=1; i<=n; i++){ if(inDegree[i] == 0){ q.push(i); res.push_back(i); } } while(q.size() != 0){ int top = q.front(); q.pop(); for(int v=1; v<=n; v++){ if(G[top][v]){ inDegree[v]--; if(inDegree[v] == 0){ q.push(v); res.push_back(v); } } } } } int main(){ fill(inDegree, inDegree+maxn, 0); fill(G[0], G[0]+maxn*maxn, false); scanf("%d%d", &n, &m); int u, v; // 这里u,v可能会重复 for(int i=0; i<m; i++){ scanf("%d%d", &u, &v); // 之前连过就不管了 if(!G[u][v]){ G[u][v] = true; inDegree[v]++; // v点入度加1 } } topoSort(); // 题目说了数据保证有解 for(int i=0; i<res.size(); i++){ printf("%d", res[i]); if(i != n-1) printf(" "); } return 0; } /* input: 5 10 3 5 1 4 2 5 1 2 3 4 1 4 2 3 1 5 3 5 1 2 output: 1 2 3 4 5 */【题目】 【解题思路】
这道题考察错位重排,百度写的很好理解。
设n个球全放错的情况有 s(n)种,那么可以有如下思路: 不妨设1号球选择2号盒,接下来会有两种情况:
2号球选择1号盒,剩下 (n-2)个球去错排,有 s(n-2) 种情况;2号球不选择1号盒,则题目可转化为把编号为2→n的小球分别放入编号为 1、3、4……→n的盒子错位重排(即2号球不在1号盒、3号球不在3号盒…n号球不在n号盒),相当于n-1个球错位重排,有s(n-1) 种;因为1号球可以放到[2,n]中任意一个盒子里,共 (n-1)种选择,于是有递推公式: s ( n ) = ( n − 1 ) ∗ [ s ( n − 1 ) + s ( n − 2 ) ] s(n)=(n-1) *[s(n-1)+s(n-2)] s(n)=(n−1)∗[s(n−1)+s(n−2)]
上面讲解来自百度百科,可以继续化简得到只依赖于n的表达式。
【源代码】
#include<iostream> using namespace std; const int maxn = 22; long long s[maxn]; //n位错位重排的数目 void solve(){ s[1] = 0; s[2] = 1; // 2 1 for(int i=3; i<=20; i++){ s[i] = (i-1)*(s[i-1]+s[i-2]); } for(int i=1; i<=20; i++){ printf("%lld\n", s[i]); } } int main(){ solve(); int n, m; scanf("%d", &n); for(int i=0; i<n; i++){ scanf("%d", &m); printf("%lld\n", s[m]); } return 0; }