2021 HZNU Winter Training Day 18

Solutions


A. 星球大战starwar

题意:
给定 n n n个点和 m m m条边,点用0 ~ n - 1编号。然后依次摧毁 k k k个点,问在摧毁第 0 0 0~ k k k个点后图中有多少个连通块。

思路:
查找连通块数量肯定不能每次都 O ( n ) O(n) O(n)查找,并且在并查集中删除点比较难处理,所以我们选择倒序处理,每次往并查集中加入点。在处理连通块数量时要选择常数较小的方法,不然很容易就T了。我们在连接两个点的时候可以判断一下这两个点是否已属于同一个并查集,如果是不同,就会使图中的连通块数量减少。

#include <bits/stdc++.h>

using namespace std;

#define endl "\n"

typedef long long ll;

const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int N = 4e5 + 7;

int n, m, u, v, k;
int fa[N], dam[N], ans[N], damQ[N];
vector<int> G[N];

int find(int x) {
	return x == fa[x]? x : fa[x] = find(fa[x]);
}

void merge(int x, int y) {
	x = find(x), y = find(y);
	if (x != y) fa[x] = y;
}

int main() {
    scanf("%d %d", &n, &m);
	for (int i = 0; i < n; ++i) fa[i] = i;
	while (m--) {
        scanf("%d %d", &u, &v);
        G[u].push_back(v);
		G[v].push_back(u);
	}
	scanf("%d", &k);
	for (int i = 1; i <= k; ++i) {
		scanf("%d", &u);
		damQ[i] = u;
		dam[u] = 1;
	}

    int res = n - k;
	for (int i = 1; i <= n; ++i) {
		if (dam[i]) continue;
		for (auto j : G[i]) {
			if (dam[j]) continue;
			if (find(i) != find(j)) res--;
			merge(i, j);
		}
	}

	ans[k] = res;
	for (int i = k - 1; i >= 0; --i) {
		u = damQ[i + 1];
        dam[u] = 0;
		res++;
        for (auto v : G[u]) {
			if (dam[v]) continue;
            if (find(u) != find(v)) res--;
            merge(u, v);
		}
		ans[i] = res;
	}
	
	for (int i = 0; i <= k; ++i) {
		printf("%d\n", ans[i]);
	}
	return 0;
}

B. Median

题意:
给你一个各个位置的值都不相同的矩阵,让你求在所有 h ∗ w h * w hw 的子矩阵中的最小的中位值是多少。

思路:
很容易想到用滑动窗口辅助区间移动,求区间第 k k k大值的做法。但是这种做法比较考验码力,同时时间复杂度也不够优秀,会被卡TLE。我们要求子矩阵中的最小的中位值,那么就是要求一个值,让它只在一个子矩阵中存在一半的值比这个值要大,其余的子矩阵中比这个值大的值都超过一半。所以我们考虑二分答案,将矩阵中大于这个值的数字置 1 1 1,其余置 0 0 0,维护二维前缀和,即可进行check

#include <bits/stdc++.h>
using namespace std;

#define endl "\n"
typedef long long ll;
const int maxn = 1e3 + 7;
int n, m, h, w;
int a[maxn][maxn], pre[maxn][maxn];

bool check(int v) {
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            if (a[i][j] > v) pre[i][j] = 1;
            else pre[i][j] = 0;
            pre[i][j] += pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1];
        }
    }
    for (int i = h; i <= n; ++i) {
        for (int j = w; j <= m; j++) {
            int tmp = pre[i][j] - pre[i - h][j] - pre[i][j - w] + pre[i - h][j - w];
            if (tmp <= h * w / 2) {
                return true;
            }
        }
        
    }
    return false;
}
void solve() {
    cin >> n >> m >> h >> w;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            cin >> a[i][j];
        }
    }
    int ans;
    int l = 1, r = 1000000, mid;
    while (l <= r) {
        mid = l + r >> 1;
        if (check(mid)) {
            ans = mid;
            r = mid - 1;
        }
        else l = mid + 1;
    }
    cout << ans << endl;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);cout.tie(nullptr);
    int _T = 1;
    // cin >> _T;
    while (_T--) solve();
}

C. Obtain Two Zeroes

题意:
给你两个正整数a, b,你每次可以选择一个正整数x,进行如下一个操作:

  • a : = a − x , b : = b − 2 x a := a - x, b := b - 2x a:=ax,b:=b2x
  • a : = a − 2 x , b : = b − x a := a - 2x, b := b - x a:=a2x,b:=bx

你可以进行任意次操作,问你能否最终让两个数都为 0 0 0
思路:
对于所有的操作,我们都可以将它们合在一起。我们令 a ≤ b a \leq b ab,设两个非负整数 n , m , a = n + 2 m , b = 2 n + m n,m,a = n + 2m, b = 2n + m nma=n+2m,b=2n+m

易得 ( n + m ) (n + m) (n+m)mod 3 = 0 3 = 0 3=0。再对 a a a进行观察,发现 2 a = 2 n + 4 m ≥ b 2a = 2n + 4m \geq b 2a=2n+4mb。所以我们判断下这两个条件就可以了。

#include <bits/stdc++.h>

using namespace std;

int n, m;

void solve() {
    scanf("%d %d", &n, &m);
    if (n > m) swap(n, m);
    if ((n + m) % 3 == 0 && n * 2 >= m) puts("YES");
    else puts("NO");
}

int main() {
    int t = 1;
    scanf("%d", &t);
    while (t--) solve();
}

D. Naive Operations

题意:
给你一段区间,接下来有两种操作。

  • add l r: 将 [ l , r ] [l, r] [l,r]区间里的值都 + 1 +1 +1
  • query l r: 输出 ∑ i = l r [ a i / b i ] \sum_{i = l}^{r}[a_i / b_i] i=lr[ai/bi]

思路:
区间问题使用线段树进行处理,那么我们要如何更新区间的值呢?因为只有当 a i > b i a_i > b_i ai>bi时,才会对答案有贡献,所以我们记录下区间内 a i a_i ai的最大值以及 b i b_i bi的最小值,那么当 a i a_i ai的最大值大于 b i b_i bi的最小值的时候,说明当前这段区间内可能存在点产生了贡献。这时我们进行向下查询,如果找到了对应的叶子节点,就进行更新。这里需要注意,我们要增加叶子节点的 b i b_i bi的值,而不是减少 a i a_i ai的值。例如 b 1 b_1 b1 2 2 2 b 2 b_2 b2 3 3 3,我们对 [ 1 , 2 ] [1, 2] [1,2]区间进行两次 a d d add add操作后,如果减小 a 1 a_1 a1的值,这段区间内 a i a_i ai的最大值为 2 2 2,而 b i b_i bi的最小值为 2 2 2,我们又要下去更新,但如果增加 b 1 b_1 b1的值就不会出现这种情况。由此得出,更新 b i b_i bi的值能让多余的递归操作的次数减少。

本标程的时间复杂度并不优秀,但是我调不动了,所以仅供参考。

#include <bits/stdc++.h>

using namespace std;

#define ls (id << 1)
#define rs (id << 1 | 1)
#define mid (l + r >> 1)

template <class T> inline void read(T &x) {
    int f = 0; x = 0; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) f |= (ch == '-');
    for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
    if (f) x = -x;
}

typedef long long ll;

const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int N = 1e5 + 7;

int b[N], n, m;

struct Seg {
    int t[N << 2], topp[N << 2], bott[N << 2], lazy[N << 2];

    void up(int id) {
        topp[id] = max(topp[ls], topp[rs]);
        bott[id] = min(bott[ls], bott[rs]);
        t[id] = t[ls] + t[rs];
    }

    void down(int id, int sz) {
        if (lazy[id] == 0) return;
        lazy[ls] += lazy[id];
        lazy[rs] += lazy[id];
        topp[ls] += lazy[id];
        topp[rs] += lazy[id];
        lazy[id] = 0;
    }

    void work(int id, int l, int r) {
        if (l == r) {
            bott[id] += b[l];
            t[id] += 1;
            return;
        }
        if (bott[id] > topp[id]) return;

        down(id, r - l + 1);

        if (topp[ls] >= bott[ls]) work(ls, l, mid);
        if (topp[rs] >= bott[rs]) work(rs, mid + 1, r);
        up(id);
    }

    void build(int id, int l, int r) {
        t[id] = topp[id] = bott[id] = lazy[id] = 0;
        if (l == r) {
            bott[id] = b[l];
            return;
        }

        build(ls, l, mid);
        build(rs, mid + 1, r);
        up(id);
    }

    void modify(int id, int l, int r, int ql, int qr) {
        if (l >= ql && r <= qr) {
            lazy[id] += 1;
            topp[id] += 1;
            if (topp[id] >= bott[id]) {
                work(id, l, r);
            }
            return;
        }

        down(id, r - l + 1);

        if (ql <= mid) modify(ls, l, mid, ql, qr);
        if (qr > mid) modify(rs, mid + 1, r, ql, qr);
        up(id);
    }

    int query(int id, int l, int r, int ql, int qr) {
        int res = 0;
        if (l >= ql && r <= qr) return t[id];
        down(id, r - l + 1);
        if (ql <= mid) res += query(ls, l, mid, ql, qr);
        if (qr > mid) res += query(rs, mid + 1, r, ql, qr);
        return res;
    }
}seg;

int main() {
    char s[11];
    int ql, qr;
    while (~scanf("%d %d", &n, &m)) {
        for (int i = 1; i <= n; ++i) scanf("%d", &b[i]);
        seg.build(1, 1, n);

        while (m--) {
            scanf("%s", s);
            scanf("%d %d", &ql, &qr);
            if (s[0] == 'a') {
                seg.modify(1, 1, n, ql, qr);
            } else {
                printf("%d\n", seg.query(1, 1, n, ql, qr));
            }
        }
    }
    return 0;
}

E. Co-prime

题意: 求区间 [ a , b ] [a,b] [a,b]中与 p p p互质的数的个数

思路: 转化为求区间 [ 1 , a − 1 ] [1,a - 1] [1,a1] [ 1 , b ] [1,b] [1,b] 上与 p p p 互质的个数的差,至于如何求这个数量,可以通过容斥原理求不互质的个数,然后用区间长度减去,就是互质的个数了。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
int t;
ll a, b, n, p[10000], num, q[10000];
void cal(ll x) {
	memset(p, 0, sizeof(p));
	num = 0;
	for(ll i = 2; i * i <= x; i++) {
		if(x % i == 0) {
			p[++num] = i;
			while(x % i == 0) x /= i;
		}
	}
	if(x > 1) p[++num] = x;
}
ll run(ll x) {
	memset(q, 0, sizeof(q));
	int t = 0, k;
	q[t++] = -1;
	for(int i = 1; i <= num; i++) {
		k = t;
		for(int j = 0; j < k; j++) {
			q[t++] = q[j] * p[i] * (-1);
		}
	}
	ll sum = 0;
	for(int i = 1; i < t; i++) {
		sum += x / q[i];
	}
	return sum;
}
void solve() {
	cal(n);
	ll cnt1 = run(a - 1);
	ll cnt2 = run(b);
	printf("%lld\n", (b - a + 1) - (cnt2 - cnt1));
}
int main() {
	scanf("%d", &t);
	for(int cas = 1; cas <= t; cas++) {
		scanf("%lld %lld %lld", &a, &b, &n);
		printf("Case #%d: ", cas);
		solve();
	}
	return 0;
}

F. Lightning

题意: 平面上有 n n n个机器人,他们之间相连的条件是距离小于 r r r,并且中间不能有其他机器人。

题解: 暴力建边,枚举每两个点,判断这两个点是否距离满足条件,并且判断这两个点构成的线段上有没有其他机器人,如果这两个条件都满足则建边,然后用矩阵树定理求生成树个数。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 310;
const int mod = 10007;
const double pi = acos((double)(-1));
#define eps 1e-8
int n, t;
double r;
ll b[maxn][maxn];
int sgn(double x){
    if(fabs(x) < eps) return 0;
    else return x < 0?-1:1;
}
struct Point{
    double x,y;
    Point(){}
    Point(double x,double y):x(x),y(y){}
    Point operator + (Point B){return Point(x + B.x,y + B.y);}
    Point operator - (Point B){return Point(x - B.x,y - B.y);}
    Point operator * (double k){return Point(x*k,y*k);}
    Point operator / (double k){return Point(x/k,y/k);}
    bool operator == (Point B){return sgn(x - B.x) == 0 && sgn(y - B.y) == 0;}
}P[maxn];
typedef Point Vector;
double Dist(Point A,Point B){
    return sqrt((A.x - B.x)*(A.x - B.x) + (A.y - B.y)*(A.y - B.y));
}
struct Line{
    Point p1,p2;
    Line(){};
    Line(Point p1,Point p2):p1(p1),p2(p2){}
    Line(Point p,double angle){
        p1 = p;
        if(sgn(angle - pi/2) == 0){p2 = (p1 + Point(0,1));}
        else {p2 = (p1 + Point(1,tan(angle)));}
    }
    Line(double a,double b,double c){
        if(sgn(a) == 0){
            p1 = Point(0, -c/b);
            p2 = Point(1, -c/b);
        }
        else if(sgn(b) == 0){
            p1 = Point(-c/a,0);
            p2 = Point(-c/a,1);
        }
        else{
            p1 = Point(0,-c/b);
            p2 = Point(1,(-c - a)/b);
        }
    }
}L[maxn];
double Cross(Vector A,Vector B){return A.x*B.y - A.y*B.x;}
double Dot(Vector A,Vector B){return A.x*B.x + A.y*B.y;} 
bool Point_on_seg(Point p, Line v){
    return sgn(Cross(p - v.p1, v.p2 - v.p1)) == 0 && 
    sgn(Dot(p - v.p1, p - v.p2)) <=0;
}
bool check(int i, int j) {
    if(sgn(Dist(P[i], P[j]) - r) > 0) return false;
    Line l = Line(P[i], P[j]);
    for(int k = 0; k < n; k++) {
        if(k == i || k == j) continue;
        if(Point_on_seg(P[k], l) == 1) return false;
    }
    return true;
}
ll det(int n){    //求n阶行列式
	ll ret = 1;
	for(int i = 0; i < n; i++){
		for(int j = i + 1; j < n; j++){
			while(b[j][i]){
				ll t = b[i][i] / b[j][i];
				for(int k = i; k < n; k++){
					b[i][k] = (b[i][k] - t * b[j][k] % mod + mod) % mod;
					swap(b[i][k], b[j][k]);
				}
				ret = (-ret + mod) % mod;
			}
		}
		ret = ret * b[i][i] % mod;
	}
	return (ret + mod) % mod;
}
int main() {
    scanf("%d", &t);
    while(t--) {
        scanf("%d %lf", &n, &r);
        memset(b, 0, sizeof(b));
        for(int i = 0; i < n; i++)
            scanf("%lf %lf", &P[i].x, &P[i].y);
        for(int i = 0; i < n - 1; i++) {
            for(int j = i + 1; j < n; j++) {
                if(check(i, j)) {
                    b[i][i]++;
                    b[j][j]++;
                    b[i][j]--;
                    b[j][i]--;
                }
            }
        }
        ll ans = det(n - 1);
        if(ans == 0)
            printf("-1\n");
        else
            printf("%lld\n", ans);
    }
    return 0;
}

G. Symmetric Matrix

题意: 给出 n n n 2 × 2 2×2 2×2的矩阵,问能不能拼成 m × m m×m m×m的对称矩阵,矩阵可以使用无数次

题解: m m m为奇数肯定不能, m m m为偶数时,如果有一个矩阵是对称矩阵,那么都用那一块矩阵也能拼成一个对称矩阵

#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
int t,n,m,a,b,c,d;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>t;
    while(t--){
        cin>>n>>m;
        int f = 0;
        for(int i = 1;i <= n; i++){
            cin>>a>>b;
            cin>>c>>d;
            if(b == c) f = 1;
        }
        if(m % 2) f = 0;
        if( f ) cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
	return 0;
}

H. Rock, Paper, Scissors

题意: 两个人在玩剪刀石头布,每个人出剪刀、石头、布的次数已经给出,问第一个人最少和最多赢多少次。

题解: 想让第一个人每次都赢,那必须第一个人出剪刀,第二个人出布,第一个人出石头,第二个人出剪刀,第一个人出布,第二个人出石头。想让第一个人赢的少,比如第一个人出剪刀,第二个人出石头赢他,如果石头不够,那出剪刀和他平局,如果还不够才出布,尽量不能让第一个人赢,这一段代码我写的挺无脑。

这题也可以用网络流来做

#include<bits/stdc++.h>
using namespace std;
const int maxn=200005;
int n,a1,a2,a3,b1,b2,b3,maxx,minn;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n;
    cin>>a1>>a2>>a3;
    cin>>b1>>b2>>b3;
    maxx = min(a1,b2) + min(a2,b3) + min(a3,b1);
    if(a1 < b3){
        a1 = 0;
        b3 = b3 - a1;
    }
    else{
        a1 = a1 - b3;
        if(a1 > b1){
            minn += a1 - b1;
            a1 = 0;
            b1 = 0;
            b2 = b2 -a1;
        }
        else{
            b1 = b1 - a1;
            a1 = 0;
        }
    }
    if(a2 < b1){
        a2 = 0;
        b1 = b1 - a2;
    }
    else{
        a2 = a2 - b1;
        if(a2 > b2){
            minn += a2 - b2;
            a2 = 0;
            b2 = 0;
            b3 = b3 -a2;
        }
        else{
            b2 = b2 - a2;
            a2 = 0;
        }
    }
    if(a3 < b2){
        a3 = 0;
        b2 = b2 - a3;
    }
    else{
        a3 = a3 - b2;
        if(a3 > b3){
            minn += a3 - b3;
        }
    }
    cout<<minn<<" "<<maxx<<endl;
	return 0;
}

I - Magic boy Bi Luo with his excited tree

题意: 在一棵既有边权又有点权的(无向)树上,要求求从每个节点出发能获得的最大权值。

  • 每经过某条边,都要花费相对应的边权。重复经过将重复花费。
  • 到达某个点,可以获得相对应的点权。只能获得一次。

题解:

题目中强调边权重复花费的时候就应该明白一点,为了得到最大权值,是需要走多条路才会走某些路往返走了两遍。

一、什么路可能会走多遍?

  • 某条支路,走过去走回来,获得点权,扣去两次边权,仍有收益。

二、什么路需要走多遍?

  • 某条支路走一个来回仍有收益。
  • 走回来以后还要走另一条有收益的支路。

现在呈现出来了一个关于支路走【来回】的问题,因此定义:

  • dp[i][0]表示从点i出发,向孩子节点移动,(不论走多少支路,反正最后)走回到i点,能获得的最大权值。
  • dp[i][1]表示从点i出发,向孩子节点移动,(不论走多少支路,反正最后)不回到i点,能获得的最大权值。

关于状态转移:

  • 假设v是u的孩子节点,并且边权为c。
  • dp[u][1]虽然最后不需要回到u点,但是如果需要走多条支路,必定有些支路需要走过去再走回来。因此对于这种情况需要选择一条合适的【末路(我自己取的名字,其他支路都走两遍,只有这条支路走过去不用回来,称为最后一条路,末路)】。dp[u][1] = max(dp[u][0] + max(0, dp[v][1] - c), dp[u][1] + max(0, dp[v][0] - 2 * c));可以理解为,一个节点下有很多孩子节点,就形成了很多支路,选择一些有收益的作为回路(去了还得回来),选择一条有收益的作为末路
  • 因为dp[u][0]最后需要回到u点,因此如果走向孩子节点v,也必须先回到v点。dp[u][0] += max(0, dp[v][0] - 2 * c);【为什么和0之间取最大值,因为有收益的支路才有走的必要】

树形DP做多了自然会比较敏感,光有对子树的遍历和记录是不够的,还需要记录向父节点移动的权值收益:

  • up[i][0]表示从i点出发,向父节点移动,(之后不管怎么走,多少支路、多少来回,反正最后)走回到i点,能获得的最大权值。
  • up[i][1]表示从i点出发,向父节点移动,(之后不管怎么走,多少支路、多少来回,反正最后)不回到i点,能获得的最大权值。

关于状态转移:

  • 假设父节点U,其下有孩子节点V1,V2,V3等,对应的边权为c1,c2,c2等。
  • 那么从孩子节点V1向上走,可以往父节点的父节点方向延申,也可以向其他的孩子节点移动。然后权值收益的计算就类似于dp部分了。
  • 简单一点的,V1点这条支路对于U点根本没有收益,本来也不在计算之内。那么状态转移最为简单:up[v1][0] = up[u][0] + dp[u][0]
  • 如果V1点恰好在U点的回路上(前面计算dp时,V1点方向作为走过去还要走回来的双程路)。dp[u][]减去V1支路部分即可:dp[v1][0] = up[u][0] + dp[u][0] - max(0, dp[v][0])
  • 最不幸的应该是V1点出现在了U点的末路上(上文有解释),如此的话V1点向上走之后单程路的最大收益需要重新计算,像求dp时一样对父节点以及孩子节点几条支路重修求一下即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
#define _rep(i,a,n) for (int i = (a); i <= (n); ++i)
#define _for(i,a,n) for (int i = (a); i <  (n); ++i)

int _, n;
int w[maxn];
int id[maxn];
int ans[maxn];

int dp[maxn][2];
int up[maxn][2];
//0 回
//1 不回 

int cnt, head[maxn];
struct edge { int v, c, next; } e[maxn << 1];
void add (int u, int v, int c) {
	e[++cnt] = {v, c, head[u]}; head[u] = cnt;
	e[++cnt] = {u, c, head[v]}; head[v] = cnt;
}

void dfs (int u, int fa) {
	id[u] = -1;
	dp[u][0] = dp[u][1] = w[u];
	for (int i = head[u]; i; i = e[i].next) {
		int v = e[i].v, c = e[i].c;
		if (v == fa) continue;
		dfs(v, u);
		int add = max(0, dp[v][0] - 2 * c);
		int tem = dp[u][0] + max(0, dp[v][1] - c);
		_rep (i, 0, 1) dp[u][i] += add;
		if (dp[u][1] < tem) {
			dp[u][1] = tem;
			id[u] = v;
		}
	}
}

void DFS (int u, int fa) {
	ans[u] = max(dp[u][0] + up[u][1], dp[u][1] + up[u][0]);
	
	for (int i = head[u]; i; i = e[i].next) {
		int v = e[i].v, c = e[i].c;
		if (v == fa) continue;
		if (v == id[u]) {
			up[v][1] = w[u] + up[u][1];
			up[v][0] = w[u] + up[u][0];
			for (int j = head[u]; j; j = e[j].next) {
				int vv = e[j].v, cc = e[j].c;
				if (vv == v || vv == fa) continue;
				int add = max(0, dp[vv][0] - 2 * cc);
				up[v][1] = max(up[v][1] + add, up[v][0] + (0, dp[vv][1] - cc));
				up[v][0] += add;
			}
		} else {
			int sub  = max(0, dp[v][0] - 2 * c);
			up[v][0] = max(0, up[u][0] + dp[u][0] - sub);
			up[v][1] = max(0, max(up[u][0] + dp[u][1], up[u][1] + dp[u][0]) - sub);
		}
		up[v][0] = max(0, up[v][0] - 2 * c);
		up[v][1] = max(0, up[v][1] - c);
		DFS(v, u);
	}
}

void run (int cas) {
	cnt = 0; memset(head, 0, sizeof head);
	
	scanf("%d", &n);
	_rep (i, 1, n) scanf("%d", w + i);
	_for (i, 1, n) { int u,  v,  c;
		scanf("%d%d%d", &u, &v, &c);
		add(u, v, c);
	}
	dfs(1, 0);
	DFS(1, 0);
	printf("Case #%d:\n", cas);
	_rep (i, 1, n) printf("%d\n", ans[i]);
}

int main () {
	scanf("%d", &_);
	_rep (i, 1, _) run(i);
	return 0;
}

J - Walking Race

题意: 题意大致是, n n n天,第 i i i天点 i i i出发,跑到距离 i i i点最远的点,记录距离。求最大的连续天数,使得其中的 最 大 距 离 − 最 小 距 离 < m 最大距离-最小距离<m <m

题解: 树形DP求解每个点的最远距离,然后线段树求区间最大最小值即可。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn = 1e6 + 10;

#define lrt rt << 1
#define rrt rt << 1 | 1

int n, m, ans;
int dp[maxn][3];

int cnt, head[maxn];
struct edge { 
	int v, w, next; 
	edge () {}
	edge (int v, int w, int next) : v(v), w(w), next(next) {}
} e[maxn << 1];
void add (int u, int v, int w) {
	e[++cnt] = edge(v, w, head[u]); head[u] = cnt;
	e[++cnt] = edge(u, w, head[v]); head[v] = cnt;
}

void dfs (int u, int fa) {
	int fir = 0, sec = 0;
	for (int i = head[u]; i; i = e[i].next) {
		int v = e[i].v, w = e[i].w;
		if (v == fa) continue;
		dfs(v, u);
		
		int c = dp[v][0] + w;
		if (c > fir) sec = fir, fir = c;
		else if (c > sec) sec = c;
	}
	dp[u][0] = fir;
	dp[u][1] = sec;
}

void DFS (int u, int fa) {
	for (int i = head[u]; i; i = e[i].next) {
		int v = e[i].v, w = e[i].w;
		if (v == fa) continue;
		if (dp[u][0] == dp[v][0] + w) dp[v][2] = max(dp[u][1], dp[u][2]) + w;
		else dp[v][2] = max(dp[u][0], dp[u][2]) + w;
		
		DFS(v, u);
	}
}

struct node { 
	int l, r, max, min;
	int mid () { return l + r >> 1; }
} T[maxn << 2];

void build (int l, int r, int rt) {
	T[rt].l = l, T[rt].r = r;
	if (l == r) return (void)(T[rt].max = T[rt].min = max(dp[l][0], dp[l][2]));
	
	int mid = T[rt].mid();
	build(l, mid, lrt);
	build(mid + 1, r, rrt);
	
	T[rt].max = max(T[lrt].max, T[rrt].max);
	T[rt].min = min(T[lrt].min, T[rrt].min);
}

int max (int l, int r, int rt) {
	if (T[rt].l >= l && T[rt].r <= r) return T[rt].max;
	
	int mid = T[rt].mid();
	int maxx = 0;
	if (l <= mid) maxx = max(maxx, max(l, r, lrt));
	if (r >  mid) maxx = max(maxx, max(l, r, rrt));
	return maxx;
}

int min (int l, int r, int rt) {
	if (T[rt].l >= l && T[rt].r <= r) return T[rt].min;
	
	int mid = T[rt].mid();
	int minn = maxn;
	if (l <= mid) minn = min(minn, min(l, r, lrt));
	if (r >  mid) minn = min(minn, min(l, r, rrt));
	return minn;
}

void run () {
	ans = 1, cnt = 0;
	memset(dp, 0, sizeof dp);
	memset(head, 0, sizeof head);
	
	for (int v = 2, u, w; v <= n; ++v) {
		scanf("%d%d", &u, &w);
		add(u, v, w);
	}
	dfs(1, 0); DFS(1, 0);
	build(1, n, 1);
	int i = 1, j = 2;
	while (i <= j && j <= n) {
		int maxx = max(i, j, 1);
		int minn = min(i, j, 1);
		if (maxx - minn <= m) ans = max(ans, j - i + 1), ++j;
		else ++i;
		if (n - i < ans) break;
	}
	printf("%d\n", ans);
}

int main () {
	while (~scanf("%d%d", &n, &m)) run();
	return 0;
}

K - Pots

题意: 还是倒水问题,这次只有两个已知容量的水杯。要求最少的步骤达到目标容量,并且输出操作方案。有一下三种操作:

  • 往其中一个水杯倒满
  • 将其中一个水杯倒完
  • 将其中一个水杯的水倒入另一个水杯

题解: BFS去模拟,每操作一次,都进行一次 p r e pre pre记录前驱状态, a n s ans ans记录操作, s t e p step step记录操作次数。达到目标容量之后通过 p r e pre pre去追溯操作方案。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#include<queue>
#include<vector>
#include<map>
using namespace std;

#define endl "\n"
#define pii pair<int, int>
#define fi first
#define se second
#define _for(i,a,n) for (int i = (a); i <  (n); ++i)
#define _rep(i,a,n) for (int i = (a); i <= (n); ++i)

typedef long long ll;
const int inf = 0x3f3f3f3f;
const int maxn = 110;

int C;
pii cup, emp;
map<pii, int> vis;
map<pii, pii> pre;
map<pii, int> step;
map<pii, string> ans;

queue<pii> Q;

void  gao (pii v, pii u) {
	vis[v] = 1;
	pre[v] = u;
	step[v] = step[u] + 1;
	Q.push(v);
}

void DFS (pii u) {
	if (pre[u] != emp) DFS(pre[u]);
	if (ans[u] != "") cout << ans[u] << endl;
}

void run () {
	Q.push(emp); vis[emp] = 1;
	while (Q.size()) {
		pii u = Q.front(); pii v = u; Q.pop();
		if (u.fi == C || u.se == C) return (void)(cout << step[u] << endl, DFS(u));	
		v.fi = cup.fi, v.se = u.se;
		if (!vis[v]) {
			gao(v, u);
			ans[v] = "FILL(1)";
		}
		v.fi = u.fi, v.se = cup.se;
		if (!vis[v]) {
			gao(v, u);
			ans[v] = "FILL(2)";
		}
		v.fi = 0, v.se = u.se;
		if (!vis[v]) {
			gao(v, u);
			ans[v] = "DROP(1)";
		}
		v.fi = u.fi, v.se = 0;
		if (!vis[v]) {
			gao(v, u);
			ans[v] = "DROP(2)";
		}
		if (u.fi) {
			if (u.fi < (cup.se - u.se)) v.fi = 0, v.se = u.se + u.fi;
			else v.fi = u.fi - (cup.se - u.se), v.se = cup.se;
			if (!vis[v]) {
				gao(v, u);
				ans[v] = "POUR(1,2)";
			}
		}
		if (u.se) {
			if (u.se < (cup.fi - u.fi)) v.fi = u.fi + u.se, v.se = 0;
			else v.fi = cup.fi, v.se = u.se - (cup.fi - u.fi);
			if (!vis[v]) {
				gao(v, u);
				ans[v] = "POUR(2,1)";
			}
		}
	}
	cout << "impossible" << endl;
}

int main () {
	emp.fi = emp.se = 0;
	cin >> cup.fi >> cup.se >> C;
	run();
	return 0;
}

L - Shortest Path

题意: 给定一个加权有向图,有n个点、m条边以及q条操作。q条操作中,有两种类型0 u表示将u点做上标记,1 u v表示计算u点和v的最短距离, 要求是最短路径上包括u、v的所有点都做上标记。 针对不同情况:

  • ERROR! At point x: 重复给x做标记则输出该语句。
  • ERROR! At path x to y: 要求最短路的两端点并未全做上标记则输出该语句。
  • No such path: 两端点都已做上标记,但是这两点间没有路。
  • 最短距离: 两点都已做上标记,并且两点间存在最短距离。

题解: n ≤ 300,所以用Floyd也许能行 。但是Q ≤ 100000绝对不是令人好受的范围。我也是尝试了很久才找到的最佳办法。

  • 多次Floyd: 这是毋庸置疑的,因为要求的最短路上的所有点都必须是做过标记的才行,所以每次有新的标记出现,我们都可能可以更新出新的最短路。但是原生的Floyd模板是不行的,三重循环的时间复杂度太高了。
  • Floyd变形: 因此我们要尽量想办法让我们的Floyd适用这个题目,并且时间复杂度能够接受。办法是缩到二重循环。 可以回忆一下Floyd的核心思想是什么,是希望能在两端点u、v之间,找到一个中间节点k,使得u->k->v的距离小于u->v的直接距离。那么我们可以在每次给出一个新的标记k时,去更新所有经过k点的最短路。、
  • 标记: 然后你还可能会犯迷糊,虽然但是,按照上面的思路,我在更新的时候,最短路的两端没做过标记怎么办,需要避开不更新吗。答案是每当出现一个新的标记k,就去更新所有可能的最短路。 而为了应对更新的最短路中两端点是为标记的情况,我们只要在输出时另加判断即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
const int inf = 0x3f3f3f3f;

int n, m, q, k, a, b, c;
int vis[N];
int dis[N][N];
void floyd(int k){
	for(int i = 0; i < n; i++)
		for(int j = 0; j < n; j++)
			dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
int main(){
	while(~scanf("%d%d%d", &n, &m, &q) && (n + m + q)){
		if(k)	puts("");
		for(int i = 0; i < n; i++){
			for(int j = 0; j < n; j++)	dis[i][j] = inf;
			vis[i] = dis[i][i] = 0;
		}
		while(m--){
			scanf("%d%d%d", &a, &b, &c);
			dis[a][b] = min(dis[a][b], c);
		}
		printf("Case %d:\n", ++k);
		while(q--){
			scanf("%d%d", &a, &b);
			if(!a){
				if(vis[b]){printf("ERROR! At point %d\n", b);continue;}	
				vis[b] = 1;
				floyd(b);
			}
			else{
				scanf("%d", &c);
				if(!vis[b] || !vis[c])	printf("ERROR! At path %d to %d\n", b, c);
				else{
					if(dis[b][c] == inf)	printf("No such path\n");
					else	printf("%d\n", dis[b][c]);
				}
			}
		}
	}
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值