BITACM2018第一轮积分赛(二)题解

A. Maze

题意概述: 给出 n×m (1n,m500) n × m   ( 1 ≤ n , m ≤ 500 ) 地图,其中 . 是空白处,# 是障碍处, 其中有 s s . 构成一整块连通块,现在要你在空白处加 k (0ks) 个障碍 # 使得地图的连通块数不变。

题解: 对空白处 DFS ,回溯的时候填满 k k 块即可。考察对DFS的理解。

参考代码:

#include <bits/stdc++.h>
using namespace std;
const int dir[4][2] = { {1, 0}, {0, 1}, {-1, 0}, {0, -1} };
char s[505][505];
bool vis[505][505];
int n, m, k;
void dfs(int x, int y) {
    vis[x][y] = 1;
    for (int i = 0; i < 4; i++) {
        int dx = x + dir[i][0];
        int dy = y + dir[i][1];
        if (dx >= 1 && dx <= n && dy >= 1 && dy <= m && !vis[dx][dy] && s[dx][dy] == '.') {
            dfs(dx, dy);
        }
    }
    if (k > 0) {
        s[x][y] = 'X';
        k--;
    }
}
int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i++) scanf("%s", s[i] + 1);
    bool ok = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (s[i][j] == '.' && !vis[i][j]) {
                dfs(i, j);
                ok = 1;
                break;
            }
        }
        if (ok) break;
    }
    for (int i = 1; i <= n; i++) puts(s[i] + 1);
    return 0;
}

B. Office Keys

题意概述: 数轴上有 n (1n103) 个人, k (0k103) k   ( 0 ≤ k ≤ 10 3 ) 把钥匙,每个人都要拿一把钥匙然后到办公室 p (0p109) p   ( 0 ≤ p ≤ 10 9 ) ,( n,p,k n , p , k 均给出坐标),每个人拿的钥匙不能相同。在数轴上移动一个单位的时间是 1 1 ,则所有人都拿到钥匙到达办公室的最短时间是多少?

题解: 我们分别读入a[1...n] b[1...k] b [ 1... k ] ,排序后观察,对于 aiaj a i ≤ a j bkbm b k ≤ b m ,如果将这两把钥匙分配给这两个人,要分配方式最优只可能 aibk,ajbm a i → b k , a j → b m ,因为如果 aibm,ajbk a i → b m , a j → b k ,那么最大的花费时间是 max(|aibm|+|bmp|,|ajbk|+|bkp|) m a x ( | a i − b m | + | b m − p | , | a j − b k | + | b k − p | ) ,显然小于前者 max(|aibk|+|bkp|,|ajbm|+|bmp|) m a x ( | a i − b k | + | b k − p | , | a j − b m | + | b m − p | ) ,白话去理解就是离门最近的人去拿最远的钥匙是不可取的

  • 根据这样分配性质,我们完全可以考虑二分答案+贪心分配,每个人都去取离他最左侧钥匙就行!这样做复杂度 O(nklogm) O ( n k l o g m ) 其中 m m 是钥匙最大长度。
  • 有了这个性质,我们容易发现这就符合线性dp的最优子结构啊,然后 n k k 的数据范围也很开心,复杂度O(nk)!(这里当做扩展,有兴趣的选手在学完动态规划以后可以来重新AC一下这道题)

参考代码:

  • 二分写法
#include <bits/stdc++.h>
#define N 1111
int a[N], b[N << 1], n, k, p;
bool check(ll x)
{
    int cnt = 0, next = 1;
    for (int i = 1; i <= n; i++)
        for (int j = next; j <= k; j++)
            if (abs(a[i] - b[j]) + abs(b[j] - p) <= x)
            {
                next = j + 1;
                cnt++;
                break;
            }
    return cnt == n;
}
int main()
{
    scanf("%d%d%d", &n, &k, &p);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= k; i++)
        scanf("%d", &b[i]);
    sort(a + 1, a + n + 1);
    sort(b + 1, b + k + 1);
    ll l = -1, r = INF, ans = 0;
    while (l <= r)
    {
        ll mid = l + r >> 1;
        if (check(mid))
        {
            ans = mid;
            r = mid - 1;
        }
        else
            l = mid + 1;
    }
    printf("%I64d\n", ans);
    return 0;
}
  • dp写法
#include <bits/stdc++.h>
#define N 1111
int a[N], b[N << 1], dp[N][N << 1];
int main()
{
    int n, k, p;
    scanf("%d%d%d", &n, &k, &p);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= k; i++)
        scanf("%d", &b[i]);
    sort(a + 1, a + n + 1);
    sort(b + 1, b + k + 1);
    dp[0][0] = 0;
    for (int i = 1; i <= k; i++)
        dp[0][i] = 0;
    for (int i = 1; i <= n; i++)
    {
        dp[i][0] = INF;
        for (int j = 1; j <= k; j++)
            dp[i][j] = max(abs(a[i] - b[j]) + abs(b[j] - p), dp[i - 1][j - 1]);
        for (int j = 1; j <= k; j++)
            dp[i][j] = min(dp[i][j], dp[i][j - 1]);
    }
    printf("%d\n", dp[n][k]);
    return 0;
}

C. Combination Lock

题目描述: 给出两个 n (1n103) n   ( 1 ≤ n ≤ 10 3 ) 位数,每次操作可以把第一个数的第 i i 位数字 ai 变成 ai(ai+1) a i → ( a i + 1 ) ,问至少进行几次操作能使第一个数变成第二个数?

题解: 直接模拟每一位,贪心地取代价的最小值 min(step,10step) m i n ( s t e p , 10 − s t e p ) 即可。

参考代码:

#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;i++)
/* head */
char a[maxm], b[maxm];
int main() {
    int n;
    scanf("%d", &n);
    scanf("%s%s", a, b);
    int ans = 0;
    rep(i, 0, n) {
        int x = a[i] - '0';
        int y = b[i] - '0';
        int z = max(x, y) - min(x, y);
        ans += min(z, 10 - z);
    }
    printf("%d\n", ans);
    return 0;
}

D. Two Buttons

题目描述: 给两个数字 n,m (1n.m104) n , m   ( 1 ≤ n . m ≤ 10 4 ) 有两种操作

  • n=n×2 n = n × 2
  • n=n1 n = n − 1

问最少进行几次操作得到 n==m n == m ?

题解: 考虑 n n m 都很小,不难想到搜索,DFS、BFS都可,考察搜索。

参考代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
/* head */
struct node {
    int x, step;
    friend bool operator< (const node& a, const node& b) { return a.step > b.step; }
};
bool vis[maxn];
int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    priority_queue<node> q;
    q.push({n, 0});
    vis[n] = 1;
    int ans = -1;
    while (!q.empty()) {
        node cur = q.top(); q.pop();
        vis[cur.x] = 1;
        if (cur.x == m) {
            ans = cur.step;
            break;
        }
        int a = cur.x << 1;
        int b = cur.x - 1;
        if (a <= m * 2  && !vis[a]) q.push({a, cur.step + 1});
        if (1 <= b && !vis[b]) q.push({b, cur.step + 1});
    }
    printf("%d\n", ans);
    return 0;
}

E. Karen and Game

题意概述: 有一个 n×m (1n,m100) n × m   ( 1 ≤ n , m ≤ 100 ) 的矩阵,每次可以对某行或某列所有元素都减去一(如果某个元素已经减到了 0 0 则后续不能对该行与该列操作),问是否存在一种删法使得整个矩阵都变成 0 ,输出最小步数,无解则输出 1 − 1

题解: 最优的肯定是每次围绕那个最小的元素操作,因此考虑从行数和列数较小的地方开始删即可,考察贪心思想。

参考代码:

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define N 111
int mp[N][N];
vector<int> ansC, ansR;
int n, m, sum1, sum2;
void solve(bool Row)
{
    if (Row)
    {
        for (int i = 1; i <= n; i++)
        {
            int cut = mp[i][1];
            for (int j = 2; j <= m; j++)
                cut = min(cut, mp[i][j]);
            for (int j = 1; j <= m; j++)
                mp[i][j] -= cut;
            sum2 += cut * m;
            while (cut--)
                ansR.push_back(i);
        }
        for (int i = 1; i <= m; i++)
        {
            int cut = mp[1][i];
            for (int j = 2; j <= n; j++)
                cut = min(cut, mp[j][i]);
            for (int j = 1; j <= n; j++)
                mp[j][i] -= cut;
            sum2 += cut * n;
            while (cut--)
                ansC.push_back(i);
        }
    }
    else
    {
        for (int i = 1; i <= m; i++)
        {
            int cut = mp[1][i];
            for (int j = 2; j <= n; j++)
                cut = min(cut, mp[j][i]);
            for (int j = 1; j <= n; j++)
                mp[j][i] -= cut;
            sum2 += cut * n;
            while (cut--)
                ansC.push_back(i);
        }
        for (int i = 1; i <= n; i++)
        {
            int cut = mp[i][1];
            for (int j = 2; j <= m; j++)
                cut = min(cut, mp[i][j]);
            for (int j = 1; j <= m; j++)
                mp[i][j] -= cut;
            sum2 += cut * m;
            while (cut--)
                ansR.push_back(i);
        }
    }
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &mp[i][j]);
            sum1 += mp[i][j];
        }
    sum2 = 0;
    solve(n <= m);
    if (sum1 != sum2)
        puts("-1");
    else
    {
        int szR = ansR.size(), szC = ansC.size();
        printf("%d\n", szC + szR);
        for (int i = 0; i < szR; i++)
            printf("row %d\n", ansR[i]);
        for (int i = 0; i < szC; i++)
            printf("col %d\n", ansC[i]);
    }
    return 0;
}

F. Online Courses In BSU

题意概述: 你有 n (1n105) n   ( 1 ≤ n ≤ 10 5 ) 门课,分别给出要修这 n n 门课之前必须修完的前置课程。给出一个人的必修课问他最少需要修几门课以及修课的顺序。若无法修完,则输出 1

题解: 有先后顺序问题,可以根据关系构造一个图,在dfs时记录经过课程的顺序。当一个需要修的课程的某个前置课程的前置课程是其本身时,该人无法修完,因此在dfs的时候记录一下到当前位置为止经过了哪些课程,如果继续经过这些课程,就无法修完。考察建图。

参考代码:

#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define SZ(x) ((int)(x).size())
#define rep(i,a,n) for (int i=a;i<n;i++)
typedef vector<int> VI;
const int maxn = 1e5 + 10;
/* head */
VI g[maxn], ans;
int a[maxn];
bool vis[maxn], over[maxn], ok = 1;
void dfs(int u) {
    if (vis[u]) ok = 0;
    if (!ok) return;
    vis[u] = 1;
    rep(i, 0, SZ(g[u])) {
        int v = g[u][i];
        if (!over[v]) {
            dfs(v);
        }
    }
    ans.pb(u);
    over[u] = 1;
}
inline void solve() {
    int n, k;
    scanf("%d%d", &n, &k);
    rep(i, 1, k + 1) scanf("%d", &a[i]);
    rep(i, 1, n + 1) {
        int t;
        scanf("%d", &t);
        while (t‐‐) {
            int j;
            scanf("%d", &j);
            g[i].pb(j);
        }
    }
    rep(i, 1, k + 1) {
        if (!over[a[i]]) dfs(a[i]);
        if (!ok) {
            puts("‐1");
            return;
        }
    }
    printf("%d\n", SZ(ans));
    rep(i, 0, SZ(ans)) {
        if (i == 0) printf("%d", ans[i]);
        else printf(" %d", ans[i]);
    }
}
int main() {
    solve();
    return 0;
}

G. Random Query

题意概述: 给一个长度为 n (1n106) n   ( 1 ≤ n ≤ 10 6 ) 的数组 a[1...n] (1ai109) a [ 1... n ]   ( 1 ≤ a i ≤ 10 9 ) ,随机从 1 1 n 中取两个数(可相等),记其中较小的数为 l l ,较大的数为 r,求数组 a a 中位于区间 [l,r] 中不同元素的个数的期望。

题解: 对每个元素,单独考虑其对答案的贡献。对于一段区间中重复出现的数字,可认为是其第一次出现时,即最左边的数字做的贡献。取法一共有 n×n n × n 种(注: l=1,r=2 l = 1 , r = 2 12 12 21 21 两种取法,但 l=r=1 l = r = 1 只有 11 11 一种取法),接下来考虑单个元素 ai a i ,其对答案有贡献的取法有多少种。对答案有贡献,等价于其在区间内且该数是区间内第一次出现,因此 l l 的可取值即为从当前位置开始,向左直到该数再次出现或到数组头,r 的可取值即为从当前位置开始,向右直到数组尾。记当前位置为 x x ,该数上一次出现位置为 p,则该元素对答案有贡献的取法种类为 2·(xp)·(nx+1)1 2 · ( x − p ) · ( n − x + 1 ) − 1 ,对答案的贡献即为 2(xp)(nx+1)1n2 2 ( x − p ) ( n − x + 1 ) − 1 n 2 。小优化,考虑到 1ai106 1 ≤ a i ≤ 10 6 ,可开 106 10 6 的数组存下每个数出现的位置,就可 O(1) O ( 1 ) 访问该数上一次出现的位置。

参考代码:

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
int n, a, b[1000010];
double ans;
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a);
        ans += (2.0 * (i - b[a]) * (n - i + 1) - 1) / n / n;
        b[a] = i;
    }
    printf("%.6f\n", ans);
    return 0;
}

H. Company Income Growth

题意概述: 给出 2001 2001 2000+n (1n100) 2000 + n   ( 1 ≤ n ≤ 100 ) 年的每年账单序列,要你找出最长的从 1 1 开始单步上升(每次步长为1)的序列长度,并输出个个年份。

题解: 看看 n 才多少?你应该知道怎么做了。

#include <bits/stdc++.h>
using namespace std;
/* head */
int main() {
    int n;
    cin >> n;

    vector <int> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];

    vector <int> res;

    int i = 0, c = 1;
    while (i < n) {
        if (a[i] == c) {
            res.push_back(2000 + i + 1);
            c++;
        } else i++;
    }
    if (res.size() == 0)
        cout << 0 << endl;
    else {
        cout << res.size() << endl;
        for (int i = 0; i < res.size(); i++)
            cout << res[i] << " ";
    }

    cout << endl;
    return 0;
}

I. New Year Snowmen

题意概述: 每个雪人由三个不同大小的雪球做成(大中小且不等),现给出 n1n105) n ( 1 ≤ n ≤ 10 5 ) 个半径为 ri1ri109) r i ( 1 ≤ r i ≤ 10 9 ) 的雪球,问最多能做出多少个雪球,并分别输出每个雪人的三个雪球的半径。

题解: 二分答案,判断mid是否可行时每次贪心地先把最大的mid个数放在最下层,然后再放第二波mid大的数,然后再放第三波mid大的数,中间遇到不符的直接跳过那个数。同时每次记录方案。

参考代码:

#include<bits/stdc++.h>
const int N = 1e5 + 10;
using namespace std;

int n;
int a[N], b[N], c[N], s[N]; // a,b,c是中间用来判断的变量数组
int aa[N], bb[N], cc[N];
int ok(int mid) {
    int i = n, j = 1;
    for(; j <= mid; i--, j++)  a[j] = s[i]; //底层
    j = 1;
    while(j <= mid && i >= 1) {
        if(s[i] < a[j]) b[j] = s[i], j++; //中层
        i--;
    }
    if(i < 1) return 0;
    j = 1;
    while(j <= mid && i >= 1) {
        if(s[i] < b[j]) c[j] = s[i], j++; //上层
        i--;
    }
    if(j != mid + 1) return 0;
    for(int k = 1; k <= mid; k++) aa[k] = a[k], bb[k] = b[k], cc[k] = c[k]; //记录答案
    return 1;
}
int main() {
    cin >> n;
    for(int i = 1; i <= n; i++)  cin >> s[i];
    sort(s + 1, s + 1 + n);
    int l = 0, r = n / 3, mid, ans = 0;
    while(l <= r) {
        int mid = (l + r) / 2;
        if(ok(mid)) ans = mid, l = mid + 1;
        else r = mid - 1;
    }
    cout << ans << endl;
    for(int i = 1; i <= ans; i++)  cout << aa[i] << " " << bb[i] << " " << cc[i] << endl;
    return 0;
}

J. Chain Reaction

题意概述: 一条马路上有 n (1n105) n   ( 1 ≤ n ≤ 10 5 ) 个灯塔,给出每个灯塔的位置和射程范围,激活的灯塔左侧在射程范围内的灯塔被摧毁,被摧毁的灯塔不会激活,现在在最右侧添加一个任意位置任意射程范围的灯塔,从最右侧开始激发,未被摧毁的灯塔继续激发,问被摧毁灯塔的最小值是多少?

题解: 贪心的思路,对于每个灯塔计算价值 vi=1sum v i = 1 − s u m 其中 sum s u m 为该灯塔射程范围内灯塔的价值之和(对于射程范围可以去二分),即为保留至该灯塔可以获得的收益, 那么 vi v i 的前缀和的最大值就是灯塔保留数量的最大值!所以用 n n 减去最大值就是答案。考察二分的熟练运用。

参考代码:

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define maxn 101000
using namespace std;
typedef long long ll;
int ans[maxn];
pair<int, int> p[maxn];
int main()
{
  int n;
  while (~scanf("%d", &n))
  {
    for (int i = 1; i <= n; i++)
      scanf("%d%d", &p[i].first, &p[i].second);
    sort(p + 1, p + n + 1);
    p[n + 1].first = -1;
    ans[0] = 0;
    int res = INF;
    for (int i = 1; i <= n; i++)
    {
      int last_live = lower_bound(p + 1, p + i, make_pair(p[i].first - p[i].second, -1)) - (p + 1);
      ans[i] = ans[last_live] + 1;
      res = min(res, n - ans[i]);
    }
    printf("%d\n", res);
  }
  return 0;
}

K. Sereja and Prefixes

题目描述: 现在有一个数字串,初始为空。有 2 种操作:

  • 第一种是在串的末尾加一个数字 x x
  • 第二种是在串的末尾加上当前串前 l 个数字,而且加 c c 次;

现在先进行 m 次操作,然后询问 n n 次,每次询问现在这个串的第 t 个数字是多少。

保证 1m,n105,1x105,1l105,1c104,1t1014 1 ≤ m , n ≤ 10 5 , 1 ≤ x ≤ 10 5 , 1 ≤ l ≤ 10 5 , 1 ≤ c ≤ 10 4 , 1 ≤ t ≤ 10 14

题解: 询问数据会非常大,这是因为第二种操作一次可以在后面补上最多 109 10 9 个数字,而操作数又有 105 10 5 个。但是每次操作在后面补上的数字大部分是重叠的,而且 l105 l ≤ 10 5 ,所以所有操作二加上的数字只可能来自前 105 10 5 个数字。因此只维护前 105 10 5 个数字,考虑后面部分:对于操作一,直接记录在后面这个地方出现了一个新数字。对于操作二,记录一下 l l c 的数据即可。这样可以知道第 i i 次操作之后当前串的长度 len[i]。显然添加数字的操作使 len[i] l e n [ i ] 单调递增。询问时,如果 t105 t ≤ 10 5 直接输出,否则二分询问的数字是在第几次操作中被添加的,对于操作一,直接输出它。对于操作二,已知了 l l c,直接在前 105 10 5 个数字中找即可。

参考代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll now;
ll s[100010];
int a[100010];
int v[100010];
int w[100010];
int opp[100010];
int len=100000;
int n,m,q,op,x,y;
int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%d",&op);
        if (op==1)
        {
            scanf("%d",&x);
            now++;
            if (now>len)
            {
                opp[++m]=1;
                v[m]=x;
                s[m]=now-1;
            }else a[now]=x;
        }else
        {
            scanf("%d%d",&x,&y);
            now+=(ll)x*y;
            if (now>len)
            {
                opp[++m]=2;
                v[m]=x;w[m]=y;
                s[m]=now-x*y;
            }
            if(now-x*y<=len)
            {
                now-=x*y;
                for (int i=1;i<=y;i++)
                    for (int j=1;j<=x;j++)
                        if (now+(i-1)*x+j<=len)
                            a[now+(i-1)*x+j]=a[j];
                            else break;
                now+=x*y;
            }
        }
    }
    scanf("%d",&q);
    while (q--)
    {
        ll t;scanf("%lld",&t);
        if (t<=len){printf("%d ",a[t]);continue;}
        int l=1,r=m,pos=m;
        while (l<=r)
        {
            int mid=(l+r)>>1;
            if (t>s[mid])pos=mid,l=mid+1;
            else r=mid-1;
        }
        if (opp[pos]==1)printf("%d",a[v[pos]]);
        else
        {
            int pp=v[pos],qq=w[pos];
            ll des=t-s[pos];des%=pp;
            if (des==0)des=pp;
            printf("%d",a[des]);
        }
        printf(" ");
    }
    puts("");
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值