2022年xtu程序设计实践3
前言:
发现光做题没有用,还是要写题解不断的回顾做题过程,做好总结。
oj动不动就T,是真的拉
最新更新:
2022-5-7 更新第 1 , 8 , 18 , 19 , 20 1,8,18,19,20 1,8,18,19,20 题。
2022-5-9 更新第 14 , 15 , 16 , 17 14,15,16,17 14,15,16,17 题。
2022-5-11 更新第 10 10 10 题。
2022-5-19 更新第 2 2 2 题。
1-逆序数(大数据)(套路题)
老套路了。逆序对 【总结】
- 归并排序,合并的时候处理贡献
- 树状数组
死活T掉了 - 线段树
- 排序二叉树
建议是平衡树。。。。
贴一份归并的逆序对,很早之前写的,不怕被查重(狗头保命)
#include <cstdio>
using namespace std;
const int lim = 10003;
int n;
int a[lim],k[lim];
int ans;
int read()
{
int res = 0;
char c = getchar();
while(c > '9' || c < '0') c = getchar();
while(c >= '0' && c <= '9')
{
res = res * 10 + c - 48;
c = getchar();
}
return res;
}
void sort_gb(int l,int r)
{
if (l == r) return;
int mid = (l + r) >> 1;
sort_gb(l,mid); sort_gb(mid+1,r);
int i = l, j = mid + 1, p = l;
while(i <= mid || j <= r)
{
if (i > mid) k[p++] = a[j++];
else if (j > r) k[p++] = a[i++], ans += r - mid;
else
{
if (a[i] <= a[j]) k[p++] = a[i++], ans += j - mid - 1;
else k[p++] = a[j++];
}
}
for (p = l; p <= r; ++p)
a[p] = k[p];
}
int main()
{
while(scanf("%d", &n) == 1)
{
if (n == 0) break;
for (int i = 1; i <= n; ++i)
a[i] = read();
ans = 0;
sort_gb(1,n);
printf("%d\n",ans);
}
return 0;
}
2-Shortest Path (一点难)
考虑按顺序经过 { 1 , a 1 , a 2 , a 3 . . a k , n } \{1, a_{1}, a_{2}, a_{3}..a_{k},n \} {1,a1,a2,a3..ak,n} 个点,并且这 k + 2 k + 2 k+2 个点都只能经过一次,那么我们考虑跑 k + 1 k+1 k+1 次最短路,每一次最短路都不能经过这 k + 2 k +2 k+2 个点,除了起点和终点。
比如,我们要顺序经过 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4 ,然后保证总路径最短,我们可以分解为: 1 − 2 1-2 1−2, 2 − 3 2-3 2−3, 3 − 4 3-4 3−4,这三段,跑三次最短路即可。但是要注意的是,在跑最短路的时候是禁止经过 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4 这四个点的,当然每一段的起点和终点是可以经过的。我们在做 d i j k s t r a dijkstra dijkstra 的时候,标记一下哪些点不能用即可。
8-Black White Chess (有点难)
经典翻转棋。
前置知识 BFS介绍(图论) - OI Wiki (oi-wiki.org)
我们考虑到询问量巨大,不可能每次都进行搜索,那么就是说,我们需要预先处理出答案,然后输出。
思考过程如下:
- 反向思考,只考虑从一个询问的状态 s s s 翻转到全 0 0 0,也就说要是反向操作,就是也可也理解为从全 0 0 0 翻转到询问状态 s s s。那么我们就有了一个对所有状态来讲有共同的源点。我们可以从全 0 0 0 状态出发,进行搜索。这就是一个经典的 b f s bfs bfs,每条边的长度都是 1 1 1。
- 考虑到翻转到全 1 1 1 的状态,可以再从全 1 1 1 的状态搜索一遍,然后和全 0 0 0 的搜索取小值,如果你可以分析到全 0 0 0 和全 1 1 1 其实是一样的。那么其实搜索一遍就够了。
其实全 0 0 0 的搜索树和全 1 1 1 的搜索树是一样的,只不过每一个节点的值都是取反了而已。
- 考虑怎么搜索优雅。我们考虑二进制压缩,即将一个二进制串对应到一个整数的二进制表示,那么每一个整数就可以表示一个搜索树的节点,对于每一个操作,就是异或上一个特定的值。
给代码:
#include <cstdio>
using namespace std;
const int lim = (1 << 16) + 4;
const int Rf = (1 << 16) - 1;
const int Rp[] = {15,15<<4,15<<8,15<<12,4369,4369<<1,4369<<2,4369<<3,
51,51<<1,51<<2,51<<4,51<<5,51<<6,51<<8,51<<9,51<<10}; //特点的值,可以手推试试。
int ans[lim];
int q[lim];
#define Min(x,y) ((x)<(y)?(x):(y))
void prime() // 事先bfs搜索出所有的答案。
{
for (int i = 0; i < lim; ++i) ans[i] = 1e9;
int l = 1, r = 1,t;
q[1] = 0; // 队列
ans[0] = 0;
while(l <= r)
{
for (int i = 0; i < 17; ++i)
{
t = q[l] ^ Rp[i];
if (ans[t] == 1e9)
{
ans[t] = ans[q[l]] + 1;
q[++r] = t; // 新节点,入队
}
}
l++; // 出队
}
}
int read()
{
int res = 0;
char s[18];
scanf("%s", s);
for (int i = 0; i < 16; ++i)
res = (res << 1) + (s[i] == '1' ? 1 : 0);
return res;
}
int main()
{
prime();
int T,a,p;
scanf("%d", &T);
while(T--)
{
a = read();
p = Min(ans[a],ans[a^Rf]);
printf("%d\n",p < 1e9 ? p : -1);
}
return 0;
}
10-Blocks (思维,有点难,线性dp)
询问很多,我们可以先把所有的情况算出来。
定义
d
p
dp
dp 状态 dp[i][j][k]
为已经精确使用了 i
个积木,排布了 j
列,最后一列的积木数不超过 k
个的情况下的排列方式。
我们考虑怎么转移:
- 最后一列不超过
k
个积木,如果是不超过k-1
的状态,我们可以先从dp[i][j][k-1]
转移过来。 - 如果最后一列恰好是
k
个积木,那么前一列必然不会超过k-1
个积木,我们就从dp[i-k][j-1][k-1]
转移过来。 - 边界是
dp[0][0][0] = 1
- 转移方程:
dp[i][j][k] = dp[i][j][k-1] + dp[i-k][j-1][k-1]
考虑的到询问是 n,m
,那么答案就是 dp[n][m][n]
。由于最后一列,肯定不会超过 n
个积木,所以可以直接采用这个状态,当然最后一列最精确的上界应该是
n
−
m
∗
(
m
−
1
)
2
n- \frac {m * (m-1)}{2}
n−2m∗(m−1) 个,即前
m
−
1
m-1
m−1 列是分别摆放
1
,
2
,
3.....
m
−
1
1,2,3.....m-1
1,2,3.....m−1 个积木。
时间复杂度 O ( n 2 m ) O(n^2m) O(n2m)
d p dp dp 的转移,一般是定义合适的状态,然后按照合理的逻辑,不重不漏的找到前继状态。
多说无益,做题做多了就有感觉了。
14-Splite (一点点难)
线性
d
p
dp
dp ,定义 dp[i][j]
为将前 i
个元素分为 j
部分的最大值。
转移方程 :
// 初始dp全部定义为负无穷
dp[0][0] = 0;
for (i = 1; i <= n; ++i)
{
for (j = 1; j <= m; ++j)
{
for (l = 1; l < i; ++l)
{
dp[i][j] = max(dp[i][j], dp[l-1][j-1] + abs(a[i] - a[l]) * (i - l + 1));
}
dp[i][j] = max(dp[i][j], dp[i-1][j-1] + a[i]); // 注意特判一个元素为一部分的情况
}
}
15-消星星 (简单)
四联通的图,相邻的相同的颜色为一个整体,数有多少个块即可。
d f s dfs dfs 遍历一遍即可。
16-Anniversary (难)
首先你得了解 d i j k s t r a dijkstra dijkstra 求单源最短路,如果不会,请跳转到第20题的题解部分。
分析可知,一个校友的路径分为两部分,初始在 v v v 点 ,要从 v v v 点先到 x x x,然后从 x x x 回到 v v v 点。
我们可以构建两幅图。
一幅是原图
G
1
G_1
G1,我们得到
x
x
x 到所有点的单源最短路
d
i
s
1
dis_1
dis1。dis1[v]
的意义是:x
到 v
的最短路径。
第二幅图是将原图的边全部反向,得到
G
2
G_2
G2,我们再次得到
x
x
x 到所有点的单源最短路
d
i
s
2
dis_2
dis2,dis2[v]
的意义是:v
点到 x
的最短路径。
我们将其对应相加,dis1[v] + dis2[v]
即是 v
先到 x
再回到 v
的最短路径。
17-Train (一点点难)
一个二维的背包问题,我们定义 dp[n][i][j]
为处理完第 n
个货物,已经载重量为 i
,货位数为 j
的情况下的最优价值。
转移即可:
dp[n][i][j] = max(dp[n][i][j], dp[n - 1][i - v][j - s] + v)
参考视频 : 背包九讲专题 bilibili
18-Swap Digits (一般)
暂时没什么高论,感觉暴力就好了。
10位数,暴力枚举排列方式(最多也就
9
!
9!
9! 种排列方式),
d
f
s
dfs
dfs 枚举每一位是多少,同时用一个 vis[]
数组记录某一位有没有被用过。在
d
f
s
dfs
dfs 的过程中记录一下交换了已经多少次。然后比较是否是小于等于
K
K
K 次就行。大概用了
4
4
4 秒左右。
讲一下怎么在 d f s dfs dfs 的过程中记录已经交换的次数。这是个套路,假设从左到右,当前是放置第 i i i 位的数字,假设放的是原数字第 j j j 位的数字,那么交换的次数就要加上原数字中 1 1 1 到 j − 1 j-1 j−1 位中还没有被用过的数字次数。
举个例子:
假设原数字是 1234567 1234567 1234567,当前处理到第 3 3 3 位,目前已经得到的数字是 1763 1763 1763,假设在第四位放置的是 5 5 5,那么在原数字中 5 5 5 之前没有用过的位置的数字有 { 2 , 4 } \{ 2,4 \} {2,4} 两个,那么交换的次数就要加 2 2 2。注意我们找的是没有用过的位置的数字,而不是数字本身,因为一个数中可能有重复的数位。
最后用 s e t set set 去重就好了。
这个题应该就是 d f s dfs dfs 全排列的一个改版吧。DFS(搜索) - OI Wiki (oi-wiki.org)
给几个多余的测试样例:
1221 3
ans : 6
776432 3
ans : 34
123456789 20 // 大样例
ans : 251802
123456789 3 // 大样例
ans : 155
123456789 1000 // 9的全排列都可以,这组不符合输入规则,但是可以测试全排列对不对
362880
1 3
ans : 1
0 1 // 特判
ans : 1
1111 2
ans : 1
19-Lost Digits (中等难)
其实是个标准的数位 d p dp dp。
尽量用思想的方式描述。
首先的话要明白几个同余等式:
a
≡
p
0
m
o
d
7
a
∗
10
+
d
≡
p
1
m
o
d
7
=
>
p
1
≡
p
0
∗
10
+
d
m
o
d
7
\begin{aligned} a & \equiv p_0 \mod 7 \\ a * 10 + d & \equiv p_1 \mod 7 \\ => p_1 & \equiv p_0 * 10 + d \mod \ 7 \end{aligned}
aa∗10+d=>p1≡p0mod7≡p1mod7≡p0∗10+dmod 7
a
a
a 是一个十进制数,
d
d
d 是单个数位(0-9)。
然后我们考虑处理到第
i
i
i 位 (数值是 字符串
s
0
.
.
s
i
s_0..s_i
s0..si 组成的数),余数是
p
p
p 的时候有多少个满足条件的数。假设是有 dp[i][p]
个数。
那么如果
i
i
i 的下一位是
p
1
p_1
p1 ,那么dp[i][p]
的贡献可以转移到 dp[i+1][(p*10 + p) % p]
。
我们只需要枚举下一位是什么即可。转移这个状态即可。
由于不能有前导0,我们需要特判这个情况,同时只有一位的时候,是可以为0的。。。
没有特判,找教练要数据,被骂了
拓展阅读: 浅学数位dp
20-path (比较难)
要求的是所有的城市到最近的救援站的最小路径和。
一句话概括,类似于多源 b f s bfs bfs 的多源最短路算法,套一层 d i j k s t r a dijkstra dijkstra 即可。
我们简要的来说一下思考流程:
- 要是所有的边长是 1 1 1 ,我们应该怎么做?
显然,我们跑一遍多源 b f s bfs bfs 即可。建一个队列,先将所有的救援站入队,定于距离
dis[u] = 0
,然后跑 b f s bfs bfs,遍历到一个节点v
,如果不在队列中,那么直接赋值dis[v] = dis[u] + 1
。由于 b f s bfs bfs 可以保证每个点只访问一次,那么就对于每一个点一定是最短路。如果你了解单源最短路,那么理解这个应该很简单BFS介绍(图论) - OI Wiki (oi-wiki.org)
- 要是只有一个救援站
如果只有一个救援站,只需要求这一个救援站到所有点的最短距离即可,具体参考 d i j k s t r a dijkstra dijkstra 算法
- 多源最短路?
将两个问题组合起来,我们发现十分的契合。我们完全可以用多源 b f s bfs bfs 的思想套一层 d i j k s t r a dijkstra dijkstra 算法,具体的做法就是,在 d i j dij dij 第一次入队的过程中,将所有的救援站入队,并将
dis[u] = 0
,然后跑普通的 d i j dij dij 即可。priority_queue<pair<int,int> > q; // dijkstra 优先队列 memset(dis, 0x3f, sizeof(dis)); // 初始将每个点的距离定义为无穷大 while (k--) // k个救援站 { x = read(); // 救援站编号 dis[x] = 0; // 赋初值 q.push({0,x}); // 初始点入队 }