集训小模拟赛2
A.翻转游戏
题目描述
翻转游戏是在一个\(4*4\)的正方形上进行的,在正方形的\(16\)个格上每个格子都放着一个双面的物件。每个物件的两个面,一面是白色,另一面是黑色,每个物件要么白色朝上,要么黑色朝上,每一次你只能翻\(1\)个物件,从而由黑到白的改变这些物件上面的颜色,反之亦然。每一轮被选择翻转的物件遵循以下规则:
从\(16\)个物件中任选一个。
翻转所选择的物件的同时,所有与它相邻的左方物件、右方物件、上方物件和下方物件(如果有的话),都要跟着翻转。
以下为例:
bwbw
wwww
bbwb
bwwb
这里b表示该格子放的物件黑色面朝上、w表示该格子放的物件白色朝上。如果我们选择翻转第三行的第一个物件,那么格子状态将变为:
bwbw
bwww
wwwb
wwwb
游戏的目标是翻转到所有的物件白色朝上或黑色朝上。你的任务就是写一个程序来求最少的翻转次数来实现这一目标。
输入格式
输入文件包含\(4\)行,每行\(4\)个字符,每个字符\(w\)或\(b\)表示游戏开始时格子上物件的状态。
输出格式
输出文件仅一个整数,即从给定状态到实现这一任务的最少翻转次数。如果给定的状态就已经实现了目标就输出\(0\),如果不可能实现目标就输出:\(Impossible\)。
样例
\(input\)
bwwb
bbwb
bwwb
bwww
\(output\)
4
大致思路:
因为题目数据并不算大,可以暴力枚举水过去,但是我们提供一个效率高一点的思路:
首先我们先把第一行改成同白或同黑,这时第一行的状态就确定了,那么第二行的状态也就确定了,按此向下推,到最后一行只需要判断一下是否满足要求,更新一下答案就可以了,其实代码思路比较清晰,一行一行向下推,暴力枚举也是可以的,要是数据范围大一些可能会存在超时的可能,建议最好不要暴力枚举。
代码实现:
#include <bits/stdc++.h>
const int maxn = 4 + 5, Inf = 1 << 16;
char str[maxn][maxn];
int ans = Inf;
char tmp[maxn][maxn];
void flip(char &ch) {
if (ch == 'w')
ch = 'b';
else if (ch == 'b')
ch = 'w'; //必须是else if,不能两个if
}
int cnt;
void press(int x, int y) {
cnt++;
flip(tmp[x][y]); //主动翻转第x行第y列,下面是被动翻转
flip(tmp[x][y + 1]);
flip(tmp[x][y - 1]);
flip(tmp[x - 1][y]);
flip(tmp[x + 1][y]);
}
void calc(int x, char ch) { //目标:全ch
memcpy(tmp, str, sizeof(str)); //拷贝原数组
cnt = 0; //计算翻转次数
for (int i = 1; i <= 4; i++) //枚举每一列
if (x & (1 << (i - 1)))
press(1, i); //第一行第i列需要翻转
for (int i = 2; i <= 4; ++i) { //枚举2~4行
for (int j = 1; j <= 4; ++j) { //枚举每一列
if (tmp[i - 1][j] != ch) //如果上一行不是ch
press(i, j); //需要通过下面这个翻转
}
}
for (int i = 1; i <= 4; ++i) //如果第四行有不满足的说明此种方案无效
if (tmp[4][i] != ch)
return;
ans = std::min(ans, cnt);
}
void Solve() {
for (int i = 1; i <= 4; ++i) scanf("%s", str[i] + 1);
for (int i = 0; i <= 15; ++i) { //一行的状态是0~15
calc(i, 'w'); //第一行状态为i,变成全w
calc(i, 'b'); //第一行状态为i,变成全b
}
if (ans == Inf)
printf("Impossible\n");
else
printf("%d\n", ans);
}
int main() {
Solve();
return 0;
}
B.强掠计划
题目大意:
\(Siruseri\) 城中的道路都是单向的。不同的道路由路口连接。按照法律的规定,在每个路口都设立了一个 \(Siruseri\) 银行的 \(ATM\) 取款机。令人奇怪的是, \(Siruseri\) 的酒吧也都设在路口,虽然并不是每个路口都设有酒吧。
\(Banditji\) 计划实施 \(Siruseri\) 有史以来最惊天动地的 抢劫。他将从市中心出发,沿着单向道路行驶,抢劫所有他途径的 \(ATM\) 机,最终他将在一个酒吧庆祝他的胜利。
使用高超的黑客技术,他获知了每个 \(ATM\) 机中可以掠取的现金数额。他希望你帮助他计算从市中心出发最后到达某个酒吧时最多能抢劫的现金总数。他可以经过同一路口或道路任意多次。但只要他抢劫过某个 \(ATM\) 机后,该 \(ATM\) 机里面就不会再有钱了。 例如,假设该城中有 \(6\) 个路口,道路的连接情况如下图所示:
市中心在路口 \(1\) ,由一个入口符号 \(→\) 来标识,那些有酒吧的路口用双圈来表示。每个 \(ATM\) 机中可取的钱数标在了路口的上方。在这个例子中, \(Banditji\) 能抢劫的现金总数为 \(47\),实施的抢劫路线是:\(1-2-4-1-2-3-5\)。
输入格式
第一行包含两个整数 \(N,M\)。\(N\) 表示路口的个数,\(M\) 表示道路条数。
接下来 \(M\) 行,每行两个整数,这两个整数都在 \(1\) 到 \(N\) 之间,第 \(i+1\) 行的两个整数表示第 \(i\) 条道路的起点和终点的路口编号。
接下来 \(N\) 行,每行一个整数,按顺序表示每个路口处的 \(ATM\) 机中的钱数 \(ai\)。
接下来一行包含两个整数 \(S,P\),\(S\) 表示市中心的编号,也就是出发的路口。\(P\) 表示酒吧数目。
接下来的一行中有 \(P\) 个整数,表示 \(P\) 个有酒吧的路口的编号。
输出格式
输出一个整数,表示 Banditji 从市中心开始到某个酒吧结束所能抢劫的最多的现金总数。
样例
\(input\)
6 7
1 2
2 3
3 5
2 4
4 1
2 6
6 5
10
12
8
16
1
5
1 4
4 3 5 6
\(output\)
47
题目思路:
这道题的主要思路就是tarjan先预处理一下。就这个例子说,我们可以看到1,2,4形成了一个权环,于是便可以把他们三个缩成一个点,然后再把原有的边连起来,然后求一个从开始的点开始的单源spfa,最后枚举酒吧的钱数求最大值。思路很清晰,缩点+spfa,主要还是看模板打的熟不熟练了。
代码实现
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int Maxx = 1e7 + 10, INF = 0x3f3f3f3f;
int n, m;
int len, Head[Maxx], To[Maxx], from[Maxx], path[Maxx], t[Maxx];//cun
int len_, H[Maxx];
int low[Maxx], dfn[Maxx], stk[Maxx], top, num, belong[Maxx], cnt;//tarjan
long long w[Maxx];
bool vis[Maxx];
long long dis[Maxx];
struct Edge {
int to, next;
} edge[Maxx], E[Maxx];
void addedge1(int a, int b) {
edge[++len].to = b;
edge[len].next = Head[a];
Head[a] = len;
}
void addedge2(int a, int b) {
E[++len_].to = b;
E[len_].next = H[a];
H[a] = len_;
}
void tarjan(int u) {
low[u] = dfn[u] = ++num;
stk[++top] = u;
for (int x = Head[u]; x; x = edge[x].next) {
int v = edge[x].to;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!belong[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++cnt;
while (1) {
int x = stk[top--];
belong[x] = cnt;
w[cnt] += path[x];
if (x == u) break;
}
}
}
void spfa(int s, int n) {
queue<int> que;
for (int i = 1; i <= n; i++) dis[i] = -INF, vis[i] = 0;
dis[s] = w[s];
que.push(s);
vis[s] = 1;
while (!que.empty()) {
int u = que.front();
que.pop();
vis[u] = 0;
for (int i = H[u]; i; i = E[i].next) {
int v = E[i].to;
if (dis[v] < dis[u] + w[v]) {
dis[v] = dis[u] + w[v];
if (!vis[v]) {
que.push(v);
vis[v] = 1;
}
}
}
}
}
void solve(){
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &from[i], &To[i]);
addedge1(from[i], To[i]);
}
for (int i = 1; i <= n; i++) {
scanf("%d", &path[i]);
}
int s, P;
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
scanf("%d%d", &s, &P);
s = belong[s];
int ansnum = 0;
for (int i = 1; i <= P; i++) {
int a;
scanf("%d", &a);
if (!vis[belong[a]]) {
t[++ansnum] = belong[a];
vis[belong[a]] = 1;
}
}
for (int i = 1; i <= m; i++) {
int u = belong[from[i]], v = belong[To[i]];
if (u != v) addedge2(u, v);
}
spfa(s, cnt);
long long ans = -INF;
for (int i = 1; i <= ansnum; i++) {
ans = max(ans, dis[t[i]]);
}
printf("%lld", ans);
}
int main(){
solve();
return 0;
}
C.测绘
题目大意
为了研究农场的气候, Betsy 帮助农夫 John 做了N(1 <= N <= 100)次气压测量并按顺序记录了结果M_1--M_N(1 <= M_i <= 1,000,000), \(Betsy\) 想找出一部分测量结果来总结整天的气压分布. 她想用 \(K(1 <= K <= N)\) 个数 \(s_j(1 <= s_1 < s_2 < ... < s_K <= N)\) 来概括所有测量结果. 她想限制如下的误差:
对于任何测量结果子集,每一个非此子集中的结果都会产生误差.总误差是所有测量结果的误差之和.更明确第说, 对于每一个和所有 \(s_j\) 都不同的 \(i\) :
* 如果 \(i\) 小于 \(s_1\), 误差是: \(2 * | M_i - M_(s_1) |\)
* 如果 \(i\) 在 \(s_j\) 和 \(s_(j+1)\) 之间,误差是: \(| 2 * M_i - Sum(s_j, s_(j+1)) |\)
注: \(Sum(x, y) = M_x + M_y\) ; ( \(M_x\) 和 \(M_y\) 之和)
* 如果 \(i\) 大于 \(s_K\) ,误差为: \(2 * | M_i - M_(s_K) |\)
\(Besty\) 给了最大允许的误差 \(E (1 <= E <= 1,000,000)\) ,找出最小的一部分结果使得误
差最多为 \(E\).
输入格式
第一行: 两个空格分离的数: \(N\) 和 \(E\)
第 \(2..N+1\) 行: 第 \(i+1\) 行包含一次测量记录: \(M_i\)
输出格式
第一行: 两个空格分开的数: 最少能达到误差小于等于 \(E\) 的测量数目和使用那个测量数目能达到的最小误差.
样例
\(input\)
4 20
10
3
20
40
\(output\)
2 17
思路
首先我们要理解一点就是这道题是数量优先,能选最少的就选最少的,在数量最少的情况下再算误差的最小,理解了这个再往下看,我一开始并没有想到是个 \(dp\) 的题目,专心研究怎么暴力(啊这),但是后来仔细一想,这真的就是一个赤裸裸的线性 \(dp\),首先我们定义dp数组的含义, \(dp[i][j]\)表示的含义就是在前j个数里选i个测试点的最小误差,那么再思考一个问题,\(dp[i][j]\) 这个状态一定选了第 \(j\) 个节点作为测试点了(你品,你细品),否则这道题的转移方程就很难写...,转移方程还要注意一个方面,不能只算前j个数的误差,一定是把j之后的误差都要算上,在这里就不举例子了,转移方程:\(dp[i][j]=min(dp[i][j],dp[i-1][k]-p[k][N+1]+p[k][j]+p[j][N+1]\),其中p数组是已经预处理好之后的状态,讲到这里就没什么好说的了,首先先初始化,然后 \(dp\) 处理,具体看代码。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
long long N,E;
long long ans,dp[120][120],p[120][120];
long long a[120];
void Read_pri(){//初始化+输入数据(没写快读)
scanf("%lld%lld", &N, &E);
for(int i = 1; i <= N; i++){
scanf("%lld",&a[i]);
}
for(int i = 1; i <= N; i++){
for(int j = i + 1; j <= N; j++){
for(int k = i + 1; k < j; k++){
p[i][j] += abs(2 * a[k] - a[i] - a[j]);
}
}
for(int j = 1; j < i; j++){
p[i][0] += 2*abs(a[i]-a[j]);
}
for(int j = i + 1; j <= N; j++){
p[i][N+1] += 2*abs(a[j] - a[i]);
}
}
}
int main(){
Read_pri();
int id = N;
ans = INF;
for(int i = 1; i <= N; i++){//特判一下一个测试点是否可以满足条件
dp[1][i] = p[i][0] + p[i][N+1];
if(dp[1][i] <= E){
id = 1;
ans = min(ans, dp[1][i]);
}
}
if(id == 1){
printf("%d %lld",id, ans);
return 0;
}
for(int i = 2; i <= N; i++){
for(int j = i; j <= N; j++){
dp[i][j] = INF;
for(int k = i - 1;k < j; k++){
dp[i][j] = min(dp[i][j],dp[i - 1][k] - p[k][N + 1] + p[k][j] + p[j][N+1]);
}
if(dp[i][j] <= E){
if(i > id) continue;
else if(i < id){
id = i;
ans = dp[i][j];
}else{
ans = min(ans, dp[i][j]);
}
}
}
}
printf("%d %lld",id, ans);
return 0;
}
D.奖学金
题目描述
小张学院有 \(c\) 名学生,第 \(i\) 名学生的成绩为 \(ai\) ,要获得的奖学金金额为 \(bi\) 。
要从这 \(c\) 名学生中挑出 \(n\) 名学生发奖学金。这个神秘人物爱好奇特,他希望得到奖学金的同学的成绩的中位数尽可能大,但同时,他们的奖学金总额不能超过 \(f\) 。
输入格式
第一行有三个整数,分别表示要挑出的学生人数 \(n\) ,学生总人数 \(c\) 和奖学金总额的最大值 \(f\) 。
第 \(2\) 到第 \((c+1)\) 行,每行两个整数,第 \((i+1)\) 行的整数依次表示第 \(i\) 名学生的成绩 \(ai\) 和如果要给他发奖学金,则需要发的金额数 \(bi\) 。
输出格式
输出一行一个整数表示答案。如果无法满足神秘人的条件,请输出 \(−1\) 。
样例
\(input\)
3 5 70
30 25
50 21
20 20
5 18
35 30
\(output\)
35
\(input\)
5 6 9
4 0
4 1
6 3
8 0
10 4
10 5
\(output\)
6
说明/提示
样例 1 解释
选择成绩为 \(5\) , \(35\) , \(50\) 的三名同学,奖金总额为 \(18+30+21=69\)。
数据规模
对于 30% 的数据,保证 n≤103,c≤2×103。
对于 100% 的数据,保证 3≤n≤105,n≤c≤2×105,0≤f≤2×109,0≤ai≤2×109,0≤bi≤10^5。
大致思路
首先要保证这道题要求中位数的最大值,那么就是尽可能的放成绩尽可能最高的那个,首先用\(sort\)根据成绩来排序,一个堆把前\((n+1)/2\)的数放进大跟堆里,然后从\((n+1)/2\)开始\(for\)循环来一次枚举成绩高的,要是成绩高而且奖学金需求还比大跟堆里最大的金额少,那么一定要放进去,并把价格最高的踢出来,保证刚放进去的数是第\((n+1)/2\)个数,直到搜到第\(c-(n+1)/2\)的时候,停止,维护了当前形式的最大中位数,再从后\((n+1)/2\)个数里向前走,求出当前形式的最小中位数,然后跑一个\(for\)循环来求出最大的中位数即可。
代码实现
#include <bits/stdc++.h>
const int maxn=2e5+5;
int n,c,F;
std::priority_queue <int> q;
struct Node{
int s,w;//分数,奖金
} a[maxn];
bool cmp(const Node &a, const Node &b){
return a.s<b.s;
}
int f[maxn],g[maxn],sum;;
void Init(){
scanf("%d%d%d", &n,&c,&F);
for(int i=1;i<=c;++i)
scanf("%d%d", &a[i].s,&a[i].w);
std::sort(a+1,a+1+c,cmp);//按成绩升序
}
void Solve(){
for(int i=1;i<=n/2;++i){//成绩最低的n/2进入队列
sum+=a[i].w;//累加总奖金
q.push(a[i].w);//队列是维护奖金的大根堆
}
//f[i]:表示以i为中位数前n/2人的最小奖金
for(int i=n/2+1;i<=c;++i){
f[i]=sum;
int top=q.top();
if(top>a[i].w){//如果当前的奖金小于堆顶则交换掉
q.pop();
sum-=top;
sum+=a[i].w;
q.push(a[i].w);
}
}
sum=0;
while(!q.empty()) q.pop();
for(int i=c;i>=c-n/2+1;--i){//成绩最高的n/2进入队列
sum+=a[i].w;
q.push(a[i].w);
}
//g[i]:表示以i为中位数后n/2人的最低奖金
for(int i=c-n/2;i>=1;--i){
g[i]=sum;
int top=q.top();
if(top>a[i].w){//交换
q.pop();
sum-=top;
sum+=a[i].w;
q.push(a[i].w);
}
}
//中位数的取值范围是[n/2+1,c-n/2]
//因为要求最大中位数,所以倒序
for(int i=c-n/2;i>=n/2+1;--i)
if(a[i].w+f[i]+g[i]<=F){
printf("%d", a[i].s);
return;
}
printf("-1\n");
}
int main(){
Init();
Solve();
return 0;
}