BJTU - 研 - 算法设计与分析

BJTU-算法设计与分析

老师们开的题很经典,也挺有难度的~
值得日后认真回顾~

Class - Divide And Conquer 分治

在线编程作业

课程平台:link

Work - 1

简单:A、B、D
python 辅助解决:F、G
题解点拨思路:H
做不出:C、E

Problem A. 谁拿了最多奖学金

入门题,枚举判断每个条件

void solve(){
    int n;
    cin >> n;
    int mx = 0, sum = 0;
    string name;
    for(int i = 0; i < n; ++ i){
        string ss, gb, xb;
        int qm, bj, num;
        cin >> ss >> qm >> bj >> gb >> xb >> num;
        int tmp = 0;
        if(qm > 80 && num >= 1) tmp += 8000;
        if(qm > 85 && bj > 80) tmp += 4000;
        if(qm > 90) tmp += 2000;
        if(qm > 85 && xb == "Y") tmp += 1000;
        if(bj > 80 && gb == "Y") tmp += 850;
        sum += tmp;
        if(tmp > mx){
            mx = tmp;
            name = ss;
        }
    }
    cout << name << "\n" << mx << "\n" << sum << "\n";
    return ;
}

Problem B. 绕钉子的长绳子

题目不全,,,,但是好像是洛谷原题——P1513 绕钉子的长绳子

两种思路,目前只写了一种

  • (基础思路) 不断加入每个位置的绳长

最终的绳长等于逆时针相邻两点的欧几里得距离 + 每个钉子处的弧长
对于钉子处的弧长,考虑直线方向向量表示,均指向同一点时,\(\overrightarrow{l_1}\)\(\overrightarrow{l_2}\),则两直线夹角为 \(\cos(\theta) = \frac{\overrightarrow{l_1} \cdot \overrightarrow{l_2}}{|l_1| |l_2|}\),则钉子上对应的弧长的圆心角大小为 \(\pi - \theta\),则对应的弧长为 \(l = R \cdot (\pi - \theta)\)

void solve(){
    int n;
    double ans = 0, r;
    cin >> n >> r;
    vector<pdd> a(n + 1);
    for(int i = 0; i < n; ++ i){
        cin >> a[i].first >> a[i].second;
    }
    if(n > 1){
        for(int i = 0; i < n; ++ i){
            int id_pre = (i + n - 1) % n, id_las = (i + 1) % n;
            double dx = a[id_pre].first - a[i].first, dy = a[id_pre].second - a[i].second;
            ans += sqrt(dx * dx + dy * dy);
            double x1 = a[i].first - a[id_pre].first, y1 = a[i].second - a[id_pre].second;
            double x2 = a[i].first - a[id_las].first, y2 = a[i].second - a[id_las].second;
            double theta = acos((x1 * x2 + y1 * y2) / sqrt(x1 * x1 + y1 * y1) / sqrt(x2 * x2 + y2 * y2));
            ans += r * (acos(-1) - theta);
        }
    }else ans = 2 * r * acos(-1);
    cout << fixed << setprecision(2) << ans << "\n";
    return ;
}
  • (更为精妙的做法) 总结性质,考虑多边形内角和

相邻两点的欧几里得距离不说了
对于两两直线的夹角,考虑到所有的点可以构成 n 凸多边形,而这样的多边形内角和为 \((n - 2) \times 180^\circ\),所以所有的弧长对应的角度和也有了,所以对应的弧长也可以算了

洛谷题解区有更加清晰且醒目的题解,可以查看哈,这里只摘记思路

Problem C. 篝火晚会

P1053 [NOIP 2005 提高组] 篝火晚会

参考题解:洛谷题解 link

const int inf = 0x3f3f3f3f, N = 2e5 + 5;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;

void solve(){
    int n;
    cin >> n;
    vector<int> l(n + 1), r(n + 1);
    for(int i = 1; i <= n; ++ i){
        cin >> l[i] >> r[i];
    }
    vector<int> ori(n + 1), tar(n + 1);
    ori[1] = tar[1] = 1;
    ori[n] = n;
    tar[2] = r[1];
    for(int i = 2; i < n; ++ i){
        ori[i] = i;
        if(tar[i - 1] == l[tar[i]])
            tar[i + 1] = r[tar[i]];
        else if(tar[i - 1] == r[tar[i]])
            tar[i + 1] = l[tar[i]];
        else{
            cout << "-1\n";
            return ;
        }
    }
    int ans = 0;
    vector cnt(2, vector<int>(n + 1, 0));
    for(int i = 1; i <= n; ++ i){
        ++ cnt[0][(tar[i] - ori[i] + n) % n];
        ++ cnt[1][(tar[i] - (n + 1 - ori[i]) + n) % n];
    }
    for(int i = 0; i < n; ++ i){
        ans = max(ans, max(cnt[0][i], cnt[1][i]));
    }
    cout << n - ans << "\n";
    return ;
}

Problem D. 课堂作业-2-B

洛谷 P1532 卡布列克圆舞曲

模拟,洛谷提交注意数据范围?
因为 long int max 值为 $2^{31} - 1 = 2147483647 $,一重排成大数后就超过 int 范围了,所以起码要开 long long !

void solve(){
    auto calc = [&](ll x, int len) -> ll {
        string a = to_string(x);
        if(a.size() < len)
            a += string(len - a.size(), '0');
        string b = a;
        sort(a.begin(), a.end(), [](char x, char y){
            return x > y;
        });
        sort(b.begin(), b.end());
        ll va = stoll(a), vb = stoll(b);
        return va - vb;
    };
    ll n;
    while(cin >> n){
        vector<ll> v;
        map<ll, int> pos;
        ll x = n, len = log10(n) + 1;
        while(true){
            if(pos.count(x)){ // ans
                for(int i = pos[x]; i < v.size(); ++ i){
                    cout << v[i] << " \n"[i == v.size() - 1];
                }
                break;
            }
            v.push_back(x);
            pos[x] = v.size() - 1;
            x = calc(x, len);
        }
    }
    return ;
}

Problem E. 循环

P1050 [NOIP 2005 普及组] 循环

难,题解见洛谷题解区

实现参考题解:洛谷题解 link

主要就是从低位往高位不断枚举幂次,最多十次(这个需要查一查为什么)获得循环周期,同时利用前面的计算结果简化复杂度,,,

const int inf = 0x3f3f3f3f, N = 2e2 + 5;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;

class BigNum{
public:
    vector<int> x;
    int k;

    BigNum(int k) : x(N) {
        this->k = k;
    }
    BigNum operator*(const BigNum& b) const {
        BigNum ans(k);
        for(int i = 0; i < k; ++ i){
            for(int j = 0; j < k; ++ j){
                ans.x[i + j] += x[i] * b.x[j];
            }
        }
        // 仅考虑后k位
        for(int i = 0; i < k; ++ i){
            ans.x[i + 1] += ans.x[i] / 10;
            ans.x[i] %= 10;
        }
        for(int i = k; i < N; ++ i) ans.x[i] = 0;
        return ans;
    }
    BigNum operator*(int b) const {
        BigNum ans = *this;
        for(int i = 0; i < N; ++ i){
            ans.x[i] *= b;
        }
        for(int i = 0; i < k; ++ i){
            ans.x[i + 1] += ans.x[i] / 10;
            ans.x[i] %= 10;
        }
        return ans;
    }
    void print_str(){
        int pos = N - 1;
        while(pos >= 0 && x[pos] == 0) -- pos;
        for(; pos >= 0; -- pos){
            cout << x[pos];
        }
        cout << "\n";
    }
};

void solve(){
    string n_str;
    int k;
    cin >> n_str >> k;
    BigNum ans(k), n(k), mul(k);
    ans.x[0] = 1;
    for(int i = 0; i < n_str.size(); ++ i){
        n.x[i] = n_str[n_str.size() - 1 - i] - '0';
    }
    mul = n;
    for(int i = 0; i < k; ++ i){
        BigNum tmp = n;
        bool flag = true;
        for(int j = 1; j <= 10; ++ j){
            tmp = tmp * mul;
            if(tmp.x[i] == n.x[i]){
                ans = ans * j;
                flag = false;
                tmp = mul;
                for(int l = 1; l < j; ++ l){
                    mul = mul * tmp;
                }
                break;
            }
        }
        if(flag){
            cout << -1 << "\n";
            return ;
        }
    }
    ans.print_str();
    return ;
}

Problem F. 整数分解(版本2)

相同题:vijos 整数分解(版本2)

  • python \(O(n^2)\) 暴力做法
a = int(input())
b = {}
b[1] = 1
for i in range(2, a + 1):
    b[i] = i
    for j in range(1, i // 2 + 1):
        b[i] = max(b[i], b[j] * b[i - j])
print(b[a])
  • 好像有结论?

https://www.cnblogs.com/xianyue/p/6933662.html

Problem G. 最小公倍数

简单的高精度乘法 + 除法 + 更相减损法求余数

有空再利用 c++ 实现一版哈~

利用 python 实现了一版,因为 python 自带大数乘法

import math
a, b = map(int, input().split())
g = math.gcd(a, b)
print(a // g * b)

Problem H. 最小数字倍数

原题 link:vijos 最小数字倍数

需要利用到余数的性质简化解法的时间复杂度

如果说枚举所有可能的数字,每个位置上的选择为 m 个,可能的长度未知,算法的时间复杂度特别高
想要找到严格的倍数,其实也就是找余数为 0 的数。

  • 从一个小数从小到大构建可能数时,余数的转移方程为 \(r' = (r * 10 + a) \% n\),a 为末尾加上的数字。
  • 考虑余数的出现情况,从零开始逐步末尾添加数字,每个余数第一次出现时对应的数字是所有这个余数中最小的(这一点非常重要,这一点将算法的枚举范围缩小到了 O(n))

基于此,考虑所有余数的转移,记录下每一个余数对应的末尾数字以及从何余数转移而来,第一次出现余数 0 意味着严格倍数的出现,反向推出答案即可

详见代码~

const int inf = 0x3f3f3f3f, N = 5e3 + 5;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;

void solve(){
    int n, m;
    cin >> n >> m;
    vector<int> a(m);
    for(int i = 0; i < m; ++ i) cin >> a[i];
    sort(a.begin(), a.end());
    vector<int> mod(n + 1), num(n + 1), pre(N, -1);
    queue<int> q;
    for(auto x : a){
        if(x){
            int r = x % n;
            mod[r] = 1;
            num[r] = x;
            q.push(r);
        }
    }
    while(q.size()){
        int u = q.front();
        q.pop();
        if(u == 0){
            string ans = "";
            while(u != -1){
                ans = to_string(num[u]) + ans;
                u = pre[u];
            }
            cout << ans << "\n";
            break;
        }
        for(auto x : a){
            int t = (u * 10 + x) % n;
            if(mod[t]) continue;
            mod[t] = 1;
            num[t] = x;
            pre[t] = u;
            q.push(t);
        }
    }
    return ;
}

Week - 2 - 分治

简单:E、F、G、H
题解点拨:A、B、C、D
感觉交大卧龙藏虎啊,大伙交的代码都非常厉害啊!好像我这虽然是 ACM 选手,但也是垫底的存在了,因为这些题基本都是看题解才做出来的

Problem A. 魔力数

参考题解:https://blog.csdn.net/zhuibushixi/article/details/109704375

思路:

  • k < 1e3 时,DP[i][j][k] ——i 位、j 魔力值、余数为 k 的方案数,dp[i + 1][j + z][(l + z * pow10[i] % m) % m] += dp[i][j][l]
  • k > 1e3 时,暴力枚举 num * m + k,次数小于 1e6 次
void solve(){
	ll n, m, d, k;
	cin >> n >> d >> m >> k;
	vector<ll> pow10(n + 1, 1);
	for(int i = 1; i <= n; ++ i) pow10[i] = pow10[i - 1] * 10;
	auto check_num = [&](ll x) -> bool {
		int cnt = d;
		while(x && cnt >= 0){
			cnt -= x % 10;
			x /= 10;
		}
		if(cnt >= 0) return true;
		else return false;
	};
	ll ans = 0;
	if(m > 1e3){
		for(ll i = k; i < pow10[n]; i += m){
			if(check_num(i)){
				++ ans;
			}
		}
	}else{
		// i 位、j 魔力值、余数为 k 的方案数
		vector dp(n + 1, vector<vector<ll>>(d + 1, vector<ll>(m, 0)));
		dp[0][0][0] = 1;
		for(int i = 0; i < n; ++ i){
			for(int j = 0; j <= d; ++ j){
				for(int l = 0; l < m; ++ l){
					for(int z = 0; z <= 9 && z <= d - j; ++ z){
						dp[i + 1][j + z][(l + z * pow10[i] % m) % m] += dp[i][j][l];
					}
				}
			}
		}
		for(int i = 0; i <= d; ++ i)
			ans += dp[n][i][k];
	}
	cout << ans << "\n";
	return ;
}

看到的暴力搜索做法,实际枚举数量才 3e6,所以也是可以过的,因为本题的 d 比较小
【25125288】,这位同学的代码每题交的都很简单啊,感觉实力很强啊!

void solve(){
    int n, m, d, k;
    cin >> n >> d >> m >> k;
    ll ans = 0;
    auto dfs = [&](auto&& self, int id, int v, ll r) -> void {
        if(id == n){
            if(r % m == k) ++ ans;
            return ;
        }
        for(int i = 0; i < 10 && i <= v; ++ i){
            self(self, id + 1, v - i, (r * 10 + i) % m);
        }
    };
    dfs(dfs, 0, d, 0);
    cout << ans << "\n";
    return ;
}

Problem B. 气球游戏

leetcode 类似题:312. 戳气球

删气球的话,得维护好每一个区间左右的气球是哪个,这个应该能做?但是挺麻烦的
逆向考虑问题!!!考虑从空序列加气球,枚举所有的可能位置即可
两种写法:记忆化搜索和 DP

// 记忆化搜索
void solve(){
    int n;
    cin >> n;
    vector<int> a(n + 2);
    a[0] = a[n + 1] = 1;
    for(int i = 1; i <= n; ++ i) cin >> a[i];
    vector dp(n + 2, vector<int>(n + 2, -1));
    auto dfs = [&](auto&& self, int l, int r) -> int {
        if(l + 1 == r) return 0;
        if(dp[l][r] != -1) return dp[l][r];
        for(int k = l + 1; k < r; ++ k){
            int sum = a[l] * a[k] * a[r];
            sum += self(self, l, k) + self(self, k, r);
            dp[l][r] = max(dp[l][r], sum);
        }
        return dp[l][r];
    };
    cout << dfs(dfs, 0, n + 1) << "\n";
    return ;
}
// DP
void solve(){
    int n;
    cin >> n;
    vector<int> a(n + 2);
    a[0] = a[n + 1] = 1;
    for(int i = 1; i <= n; ++ i) cin >> a[i];
    vector dp(n + 2, vector<int>(n + 2));
    for(int i = n - 1; i >= 0; -- i){
        for(int j = i + 2; j <= n + 1; ++ j){
            for(int k = i + 1; k < j; ++ k){
                int sum = a[i] * a[k] * a[j];
                sum += dp[i][k] + dp[k][j];
                dp[i][j] = max(dp[i][j], sum);
            }
        }
    }
    cout << dp[0][n + 1] << '\n';
    return ;
}

Problem C. 集合划分

类似题解:https://blog.csdn.net/qq_45745322/article/details/115266081

定义 \(F(n, m)\) 表示将 n 个数分成 m 个集合的种类数,则转移方程如下:$ F(n, m) = F(n - 1, m - 1) + m * F(n - 1, m) $

  • 可以前 n - 1 个数组成 m - 1 组,第 n 个数单独一组
  • 可以前 n - 1 个数组成 m 组,第 n 个数随机进组

考虑边界情况赋值即可,记得开 longlong

本题是考虑所有可能的情况,所以答案就是 \(\sum_{i=1}^{n}F(n, i)\)

void solve(){
    int n;
    map<pair<int, int>, ll> cnt;
    auto dfs = [&](auto&& self, int x, int y) -> ll {
        if(x < y || y < 1) return 0;
        if(x == y || y == n) return 1;
        if(cnt[{x, y}] == 0){
            cnt[{x, y}] = self(self, x - 1, y - 1) + self(self, x - 1, y) * y;
        }
        return cnt[{x, y}];
    };
    while(cin >> n){
        ll ans = 0;
        for(int i = 1; i <= n; ++ i){
            ans += dfs(dfs, n, i);
        }
        cout << ans << "\n";
    }
    return ;
}

Problem D. 染色问题

原题:hdu 2045 不容易系列之(3)—— LELE的RPG难题。 注意题目要求不一样!

题解:https://blog.csdn.net/GenuineMonster/article/details/88289989

假设前 n - 1 个位置的全部涂色情况都已经清楚了,设 ans(t) 表示前 t 个位置的合法涂色数。则对于第 n 位置,会存在以下两种情况:

  • 当第 1 个位置和第 n - 1 个位置涂色不同时,此时第 n 个位置只有 1 种涂色方法,前面一共有 ans(n - 1) 种涂色方法,故 ans(n) = ans(n - 1).
  • 当第 1 个位置和第 n - 1 个位置涂色相同时,此时第 n 个位置有 2 种涂色方法,前面一共有 ans(n - 2) 种涂色方法,故 ans(n) = 2 * ans(n - 2).

第二种情况可以再理解一下,相当于此时没有第 n - 1 个位置,仅考虑 n - 2 个位置时的可能情况数就是此时的总体可能情况数

结论公式

\[ans(n) = \begin{cases} 3, & \text{if n = 1} \\ 6, & \text{if n = 2} \\ 6, & \text{if n = 3} \\ ans(n - 1) + ans(n - 2) * 2, & \text{if n $\ge$ 4} \end{cases} \]

void solve(){
    vector<ll> a(N);
    a[1] = 3; a[2] = 6; a[3] = 6;
    for(int i = 4; i < N; ++ i) a[i] = (a[i - 1] + a[i - 2] * 2) % mod;
    int n;
    while(cin >> n){
        cout << a[n] << "\n";
    }
    return ;
}

Problem E. 最近点对

这份代码并没有对 n = 1 的情况加以修改与限制,我感觉 n = 1 的时候 ans 应该设置为 0 吧
思想:最近点对的仅考虑后 6 个即可

void solve(){
    int n;
    while(cin >> n){
        vector<pdd> a(n);
        for(auto& [x, y] : a){
            cin >> x >> y;
        }
        sort(a.begin(), a.end());
        auto get_dist = [&](pdd& x, pdd& y) -> double {
            return sqrt((x.first - y.first) * (x.first - y.first) + (x.second - y.second) * (x.second - y.second));
        };
        auto closeset = [&](auto&& self, int l, int r) -> double {
            if(l == r) return 2e9;
            if(r - l <= 1) return get_dist(a[l], a[r]);
            int mid = (l + r) >> 1;
            double d = min(self(self, l, mid), self(self, mid + 1, r));
            vector<pdd> v;
            for(int i = l; i <= r; ++ i){
                if(fabs(a[i].first - a[mid].first) <= d){
                    v.push_back(a[i]);
                }
            }
            sort(v.begin(), v.end(), [&](pdd& x, pdd& y){
                return x.second < y.second;
            });
            int m = v.size();
            for(int i = 0; i < m; ++ i){
                for(int j = i + 1; j < min(i + 7, m); ++ j){
                    d = min(d, get_dist(v[i], v[j]));
                }
            }
            return d;
        };
        cout << fixed << setprecision(2) << closeset(closeset, 0, n - 1) << "\n";
    }
    return ;
}

Problem F. 整数的幂次方表示

一眼分治题,花点时间调一调代码就行了

void solve(){
    int n;
    cin >> n;
    unordered_map<int, string> a;
    a[0] = "0";
    a[1] = "2(0)";
    a[2] = "2";
    auto dfs = [&](auto&& self, int x) -> string {
        if(a.count(x)) return a[x];
        string tmp = "";
        int cnt = 0, y = x;
        while(x){
            int t = x % 2;
            if(t) tmp = (cnt == 1 ? "2" : "2(" + self(self, cnt) + ")") + (tmp == "" ? "" : "+") + tmp;
            ++ cnt;
            x >>= 1;
        }
        return a[y] = tmp;
    };
    cout << dfs(dfs, n) << "\n";
    return ;
}

Problem G. 小明的散步路径

规律性非常强的一题,稍微画一下图就可以发现,如何从第 n 个散步路径转到第 n - 1 个散步路径,以此来统计答案

image

void solve(){
    int n, x, y;
    cin >> n >> x >> y;
    ll ans = 0, val = (1 << (n - 1));
    for(int i = n; i > 0; -- i){
        if(i == 1){
            ans += (x < 2 ? y : x + 3 - y);
            break;
        }
        if(x > val){ // right
            ans += val * val * 2;
            if(y > val){ // up
                x -= val;
                y -= val;
            }else{ // down
                ans += val * val;
                int t = x;
                x = val - y + 1;
                y = val * 2 - t + 1;
            }
        }else{ // left
            if(y > val){ // up
                ans += val * val;
                y -= val;
            }else{ // down
                int t = y;
                y = x;
                x = t;
            }
        }
        val >>= 1;
    }
    cout << ans << '\n';
    return ;
}

Problem H. Ricky队形

归并排序求解逆序对个数

void solve(){
    int n;
    cin >> n;
    vector<int> a(n + 1), b(n + 1);
    for(int i = 1; i <= n; ++ i) cin >> a[i];
    ll ans = 0;
    auto Merge_sort = [&](auto&& self, int s, int t) -> void {
        if(s >= t) return ;
        int mid = (s + t) >> 1;
        self(self, s, mid);
        self(self, mid + 1, t);
        int i = s, j = mid + 1, k = s;
        while(i <= mid && j <= t){
            if(a[i] > a[j]){
                b[k ++] = a[j ++];
                ans += mid - i + 1; // 在此处统计逆序对即可
            }else{
                b[k ++] = a[i ++];
            }
        }
        while(i <= mid) b[k ++] = a[i ++];
        while(j <= t) b[k ++] = a[j ++];
        for(int p = s; p <= t; ++ p) a[p] = b[p];
    };
    Merge_sort(Merge_sort, 1, n);
    cout << ans << "\n";
    return ;
}

Week - 3 - 动态规划

基础 DP :A、B、C、G
矩阵快速幂优化 DP:D
题解:E、F、H

Problem A. 晴天小猪历险记之Hill

抽象题意,,,求从山脚到山顶的最短路
“朝左、右、左上、右上四个方向走(注意:在任意一层的第一段也可以走到本层的最后一段或上一层的最后一段)” 这一段描述真的抽象,,,,

void solve(){
    int n;
    cin >> n;
    vector a(n + 1, vector<int>(1));
    for(int i = 1; i <= n; ++ i){
        for(int j = 1; j <= i; ++ j){
            int x;
            cin >> x;
            a[i].push_back(x);
        }
    }
    priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> q;
    vector dis(n + 1, vector<int>(n + 1, inf));
    vector vis(n + 1, vector<bool>(n + 1, false));
    int fx[4] = {0, 0, -1, -1}, fy[4] = {-1, 1, -1, 0};
    q.push({a[n][1], n, 1});
    while(q.size()){
        auto [d, x, y] = q.top();
        q.pop();
        if(vis[x][y]) continue;
        vis[x][y] = true;
        dis[x][y] = d;
        for(int i = 0; i < 4; ++ i){
            int xx, yy;
            xx = x + fx[i];
            if(xx <= 0 || xx > n) continue;
            if(i < 2){
                yy = (y + fy[i] + x - 1) % x + 1;
            }else{
                yy = (y + fy[i] + xx - 1) % xx + 1;
            }
            if(yy <= 0 || yy > xx) continue;
            if(!vis[xx][yy] && dis[xx][yy] > d + a[xx][yy]){
                dis[xx][yy] = d + a[xx][yy];
                q.push({dis[xx][yy], xx, yy});
            }
        }
    }
    cout << dis[1][1] << '\n';
    return ;
}

Problem B. 清帝之惑之顺治

求最长严格递减 / 递增序列长度

void solve(){
    int r, c, fx[4] = {0, 0, 1, -1}, fy[4] = {1, -1, 0, 0};
    cin >> r >> c;
    vector a(r, vector<int>(c));
    vector<tuple<int, int, int>> q;
    for(int i = 0; i < r; ++ i){
        for(int j = 0; j < c; ++ j){
            cin >> a[i][j];
            q.push_back({a[i][j], i, j});
        }
    }
    sort(q.begin(), q.end());
    vector dp(r, vector<int>(c, 1));
    int ans = 1;
    for(auto [v, x, y] : q){
        for(int i = 0; i < 4; ++ i){
            int xx = x + fx[i], yy = y + fy[i];
            if(xx < 0 || xx >= r || yy < 0 || yy >= c) continue;
            if(a[x][y] > a[xx][yy])
                dp[x][y] = max(dp[x][y], dp[xx][yy] + 1);
        }
        ans = max(ans, dp[x][y]);
    }
    cout << ans << '\n';
    return ;
}

Problem C. 小飞侠的游园方案

01背包

void solve(){
    int n, t;
    cin >> n >> t;
    vector<int> dp(t + 5);
    for(int i = 0; i < n; ++ i){
        int fi, ti;
        cin >> fi >> ti;
        for(int j = t; j >= ti; -- j){
            dp[j] = max(dp[j], dp[j - ti] + fi);
        }
    }
    cout << dp[t] << "\n";
    return ;
}

Problem D. Warcraft III 守望者的烦恼

矩阵快速幂板子题,,,,,详见代码吧~

struct Matrix{
	static const int N=15;
	ll a[N][N], n;
	Matrix(ll n, ll e=0){
		this->n = n;
		for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) a[i][j]=e*(i==j);
	}
	Matrix mul(Matrix A,Matrix B){
		Matrix ans(n, 0);
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				for(int k=1;k<=n;k++){
					ans.a[i][j]=(ans.a[i][j]+A.a[i][k]*B.a[k][j])%mod;
				}
			}
		}
		return ans;
	}
	Matrix ksm(Matrix A,ll b){
		Matrix ans(n, 1); // 单位阵
		while(b){
			if(b&1) ans=mul(ans,A);
			A=mul(A,A);b>>=1;
		}
		return ans;
	}
};

void solve(){
    ll n, k, ans = 0;
    cin >> k >> n;
    if(n <= k){
        ans = 1ll << (n - 1);
    }else{
        vector<ll> A(k + 1, 0);
        Matrix B(k, 0);
        for(int i = 1; i <= k; ++ i){
            B.a[i][1] = 1;
            A[i] = 1 << (k - i);
        }
        for(int i = 1; i < k; ++ i) B.a[i][i + 1] = 1;
        B = B.ksm(B, n - k);
        for(int i = 1; i <= k; ++ i){
            ans = (ans + B.a[i][1] * A[i] % mod) % mod;
        }
    }
    cout << ans << "\n";
    return ;
}

Problem E. 4-Hanoi-Tower

4 柱汉诺塔问题

参考题解:https://blog.csdn.net/zhuibushixi/article/details/109678374

void solve(){
    int n, ans = 1;
    cin >> n;
    -- n;
    int c = 0, k = 1, a = 2;
    while(n --){
        ans = (ans + a) % mod;
        ++ c;
        if(c == k + 1){
            ++ k;
            c = 0;
            a = (a << 1) % mod;
        }
    }
    cout << ans % mod << '\n';
    return ;
}

Problem F. 加分二叉树

洛谷 link:https://www.luogu.com.cn/problem/P1040
题解:https://www.luogu.com.cn/article/t5bgyybc
区间 DP 思维

void solve(){
    ll n;
    cin >> n;
    vector f(n + 1, vector<ll>(n + 1)), root(n + 1, vector<ll>(n + 1));
    for(int i = 1; i <= n; ++ i){
        cin >> f[i][i];
        root[i][i] = i;
    }
    for(int len = 1; len < n; ++ len){
        for(int i = 1; i + len <= n; ++ i){
            int j = i + len;
            f[i][j] = f[i + 1][j] + f[i][i];
            root[i][j] = i;
            for(int k = i + 1; k < j; ++ k){
                ll t = f[i][k - 1] * f[k + 1][j] + f[k][k];
                if(f[i][j] < t){
                    f[i][j] = t;
                    root[i][j] = k;
                }
            }
        }
    }
    cout << f[1][n] << "\n";
    auto output = [&](auto&& self, int l, int r) -> void {
        if(l > r) return ;
        cout << root[l][r] << " ";
        if(l == r) return ;
        self(self, l, root[l][r] - 1);
        self(self, root[l][r] + 1, r);
    };
    output(output, 1, n);
    return ;
}

Problem G. 装箱问题

01 背包

void solve(){
    int c, n;
    cin >> c >> n;
    vector<int> a(c + 5);
    for(int i = 0; i < n; ++ i){
        int x;
        cin >> x;
        for(int j = c; j >= x; -- j){
            a[j] = max(a[j], a[j - x] + x);
        }
    }
    cout << c - a[c] << "\n";
    return ;
}

Problem H. 买蛋糕

洛谷 link:https://www.luogu.com.cn/problem/P1490
参考题解:https://blog.csdn.net/qq_35091353/article/details/102644774

第一问想的明白,第二问抄了题解,日后有空再看吧~

void solve(){
    int n;
    cin >> n;
    int ans = __lg(n) + 1, tot = 0;
    vector f(ans + 5, vector (n + 5, vector<int>(n + 5, 0)));
    f[1][1][1] = 1;
    for(int i = 1; i < ans; ++ i){
        for(int j = i; j <= (1 << (i - 1)); ++ j){
            for(int k = i; k <= ((1 << i) - 1); ++ k){
                if(f[i][j][k]){
                    for(int p = j + 1; p <= k + 1; ++ p){
                        if(p + k <= n)
                            f[i + 1][p][k + p] += f[i][j][k];
                        else
                            f[i + 1][p][n] += f[i][j][k];
                    }
                }
            }
        }
    }
    for(int i = 1; i <= n; ++ i){
        tot += f[ans][i][n];
    }
    cout << ans << " " << tot << "\n";
    return ;
}

Work - 4 - 贪心 + 搜索

简单:B、G、H
题解:A、C、D、E、F、I

Problem A. 最小差距

题解:https://zhaopeng0103.github.io/算法题/贪心算法-最小差距/

贪心考虑问题

  • n = 2,此时两数差值绝对值即为 ans
  • n 为奇数,在保证 0 不开头的前提下,两个数的位数差应该为1,大数尽可能小,小数尽可能大
  • n 为偶数,将 N 个数排序后,任意两个相邻的均可作为两个数的开头,之后让大数尽可能小,小数尽可能大
void solve(){
    int n;
    cin >> n;
    vector<int> a(n);
    for(int i = 0; i < n; ++ i) cin >> a[i];
    sort(a.begin(), a.end());
    int ans = inf, num1, num2;
    if(n == 2){
        ans = abs(a[0] - a[1]);
    }else if(n % 2){
        if(a[0] == 0) swap(a[0], a[1]);
        num1 = num2 = 0;
        for(int i = 0; i <= n / 2; ++ i){
            num1 = num1 * 10 + a[i];
            if(i != n / 2)
                num2 = num2 * 10 + a[n - 1 - i];
        }
        ans = min(ans, abs(num1 - num2));
    }else{
        for(int i = 0; i < n - 1; ++ i){
            if(a[i] == 0) continue;
            num1 = a[i];
            num2 = a[i + 1];
            int l = 0, r = n - 1;
            for(int j = 0; j < n / 2 - 1; ++ j){
                if(l == i) l ++;
                if(l == i + 1) l ++;
                if(r == i + 1) r --;
                if(r == i) r --;
                num1 = num1 * 10 + a[r --];
                num2 = num2 * 10 + a[l ++];
            }
            ans = min(ans, abs(num1 - num2));
        }
    }
    cout << ans << '\n';
    return ;
}

Problem B. 合并果子

贪心,每次取最小的两堆进行合并,利用堆维护即可

void solve(){
    int n;
    cin >> n;
    priority_queue<int, vector<int>, greater<int>> q;
    for(int i  = 0; i < n; ++ i){
        int x;
        cin >> x;
        q.push(x);
    }
    int ans = 0;
    while(q.size() > 1){
        int x = q.top();
        q.pop();
        int y = q.top();
        q.pop();
        ans += x + y;
        q.push(x + y);
    }
    cout << ans << "\n";
    return ;
}

Problem C. 均分纸牌

原题:https://www.luogu.com.cn/problem/P1031
题解 link:https://www.luogu.com.cn/article/kjjwv38b

void solve(){
    int n, avg = 0;
    cin >> n;
    vector<int> a(n + 1);
    for(int i = 0; i < n; ++ i){
        cin >> a[i];
        avg += a[i];
    }
    avg /= n;
    int ans = 0;
    for(int i = 0; i < n; ++ i){
        if(a[i] != avg){
            ++ ans;
            a[i + 1] += a[i] - avg;
        }
    }
    cout << ans << "\n";
    return ;
}

Problem D. 上帝的爱好

贪心题,自己还是笨了

题解 link:https://blog.csdn.net/qq_35091353/article/details/102981757

  • 先观察一个点,题目中给的四种形式的词,就是把两对相同数的任意排列
  • 题目要求句子之间不能交错,说明最优解是不重叠的分块的
  • 在这样的限制下,贪心的策略就是从前往后尽可能早地寻找能匹配上的两对数,之后统计上答案,之后更新下一个合法起始位置(细细想想,这个贪心策略是有道理的,可以证明?)
void solve(){
    int n;
    cin >> n;
    vector<int> a(n);
    int cnt = 0, start = 0, ans = 0;
    for(int i = 0; i < n; ++ i){
        cin >> a[i];
        for(int j = start; j < i; ++ j){
            if(a[i] == a[j]){
                ++ cnt;
                a[i] = a[j] = 0;
                break;
            }
        }
        if(cnt == 2){
            ++ ans;
            cnt = 0;
            start = i + 1;
        }
    }
    cout << ans << '\n';
    return ;
}

Problem E. 旅行家的预算

题解link:https://www.luogu.com.cn/article/040pwhvk

贪心策略
1.枚举途中经过的加油站,每经过一个加油站,计算一次花费;
2.在一个加油站所需要加的油,就是能够支持它到达下一个油价比它低的加油站的量;
3.如果在这个加油站即使加满油,都不能到达一个比它油价低的加油站,就把油箱加满,前往能够到达的加油站中油价最低的那个;
4.如果在这个加油站即使加满油,都不能到达任意一个加油站,也不能到达终点城市,说明无解;

把起点和终点也视为加油站加到代码中

void solve(){
    double d1, c, d2, p;
    int n;
    cin >> d1 >> c >> d2 >> p >> n;
    vector<pdd> a(n);
    for(int i = 0; i < n; ++ i){
        cin >> a[i].first >> a[i].second; 
    }
    a.push_back({0, p});
    a.push_back({d1, 0});
    sort(a.begin(), a.end());
    double max_len = c * d2, ans = 0, res = 0;
    bool flag = true;
    int id = 0;
    while(id < n + 1){
        bool find_next = false;
        int f = inf;
        double dis = a[id].first;
        for(int i = id + 1; i <= n + 1 && a[i].first - dis <= max_len; ++ i){
            if(a[i].second < a[id].second){ // 第一个费用小的加油站
                ans += (a[i].first - a[id].first - res) / d2 * a[id].second;
                id = i;
                find_next = true;
                break;
            }
            if(f == inf || a[i].second <= a[f].second){ // 可达范围内费用最低的加油站
                f = i;
            }
        }
        if(find_next) continue;
        if(f == inf){ // 找不到下一个加油站
            flag = false;
            break;
        }
        ans += c * a[id].second;
        res = max_len - (a[f].first - a[id].first);
        id = f;
    }
    if(!flag) cout << -1 << "\n";
    else cout << fixed << setprecision(2) << ans << '\n';
    return ;
}

Problem F. 北京2008的挂钟

原题:https://vijos.org/p/1016

题解:https://blog.csdn.net/Look_star/article/details/109902546

观察可得:

  • 共有九种操作,因为只有四个状态,所以每种操作只需要执行0到3次就可以覆盖所有情况。(剪枝)
  • 对所有的操作进行搜索,可能情况4^9 即2^18种情况,可以暴力。
void solve(){
    string s[] = {
        "ABDE",
        "ABC",
        "BCEF",
        "ADG",
        "BDEFH",
        "CFI",
        "DEGH",
        "GHI",
        "EFHI"
    };
    int n = 9;
    vector<int> a(n), b(n), ans;
    for(int i = 0; i < n; ++ i){
        cin >> a[i];
    }
    auto dfs = [&](auto&& self, int id) -> bool {
        if(id == n){
            for(int i = 0; i < n; ++ i){
                if((a[i] + b[i]) % 4)
                    return false;
            }
            return true;
        }
        for(int c = 0; c <= 3; ++ c){
            for(auto ch : s[id]){
                b[ch - 'A'] += c;
            }
            bool f = self(self, id + 1);
            if(f){
                for(int i = 0; i < c; ++ i)
                    ans.push_back(id + 1);
                return true;
            }
            for(auto ch : s[id]){
                b[ch - 'A'] -= c;
            }
        }
        return false;
    };
    dfs(dfs, 0);
    int m = ans.size();
    for(int i = m - 1; i >= 0; -- i){
        cout << ans[i] << " \n"[i == 0];
    }
    return ;
}

Problem G. 毒药?解药?

最短路问题

  • 位运算状态压缩,共 \(2^n\) 个状态
  • 状态转移共 m 种方式
void solve(){
    int n, m;
    cin >> n >> m;
    vector a(m, vector<int>(n));
    for(int i = 0; i < m; ++ i){
        for(int j = 0; j < n; ++ j){
            cin >> a[i][j];
        }
    }
    vector<int> f(1 << n, inf);
    priority_queue<pii, vector<pii>, greater<pii>> q;
    f[(1 << n) - 1] = 0;
    q.push({0, (1 << n) - 1});
    while(q.size()){
        auto [d, s] = q.top();
        q.pop();
        if(f[s] < d) continue;
        f[s] = d;
        for(int i = 0; i < m; ++ i){
            int ns = s;
            for(int j = 0; j < n; ++ j){
                if(a[i][j] == 1){
                    ns &= ~(1 << j);
                }
                if(a[i][j] == -1){
                    ns |= (1 << j);
                }
            }
            if(f[ns] > f[s] + 1){
                f[ns] = f[s] + 1;
                q.push({f[ns], ns});
            }
        }
    }
    if(f[0] == inf) cout << "The patient will be dead.\n";
    else cout << f[0] << "\n";
    return ;
}

Problem H. 海战

搜索

题意就是判断所有四连通的 '#' 是否都构成方形

解法:遍历每一个连通块,记录最小最大行列值,判断个数是否填满连通块

void solve(){
    int r, c;
    cin >> r >> c;
    vector<string> g(r);
    for(int i = 0; i < r; ++ i){
        cin >> g[i];
    }
    vector vis(r, vector<int>(c));
    int ans = 0, fx[4] = {0, 0, 1, -1}, fy[4] = {1, -1, 0, 0};
    auto judge = [&](int x, int y) -> bool {
        queue<pii> q;
        q.push({x, y});
        int x_mn = x, x_mx = x, y_mn = y, y_mx = y, cnt = 0;
        while(q.size()){
            auto [tx, ty] = q.front();
            q.pop();
            if(vis[tx][ty]) continue;
            ++ cnt;
            vis[tx][ty] = 1;
            x_mn = min(x_mn, tx);
            x_mx = max(x_mx, tx);
            y_mn = min(y_mn, ty);
            y_mx = max(y_mx, ty);
            for(int i = 0; i < 4; ++ i){
                int xx = tx + fx[i], yy = ty + fy[i];
                if(xx >= 0 && xx < r && yy >= 0 && yy < c && !vis[xx][yy] && g[xx][yy] == '#'){
                    q.push({xx, yy});
                }
            }
        }
        if(cnt != (x_mx - x_mn + 1) * (y_mx - y_mn + 1)) return false;
        return true;
    };
    for(int i = 0; i < r; ++ i){
        for(int j = 0; j < c; ++ j){
            if(!vis[i][j] && g[i][j] == '#'){
                bool f = judge(i, j);
                if(f) ++ ans;
                else{
                    cout << "Bad placement.";
                    return ;
                }
            }
        }
    }
    cout << "There are " << ans << " ships.";
    return ;
}

Problem I. 传染病防治

原题 link:https://www.luogu.com.cn/problem/P1041
洛谷说题目有问题,被证明没有靠谱的多项式复杂度的做法

贪心砍去子树节点数最大的位置是错误的思路

解释:https://www.luogu.com.cn/article/kk4nhh7t

反例: 我们需要构造一个树,包含两个主要分支:
分支 A(长链):很长,像一条蛇。虽然每一层只有一个节点,但累加起来总节点数(\(siz\))很大。
分支 B(胖分支):很短,但很宽。比如第一层就分叉出几十个节点。总节点数故意设计得比分支 A 稍微小一点。
图解示例:假设根节点有两个儿子:\(u\)(长链头)和 \(v\)(胖分支头)。
分支 A (长链):结构:\(u \to a_1 \to a_2 \to \dots \to a_{100}\)特点:深度 100,每层 1 个。子树大小 \(siz_u = 101\)。威胁度:下一秒只会新增 1 个感染者。
分支 B (胖分支):结构:\(v\) 下面直接连了 80 个叶子节点。特点:深度 2,非常宽。子树大小 \(siz_v = 81\)。威胁度:下一秒会新增 80 个感染者。
贪心的错误结果:算法比较 \(siz_u\)\(siz_v\)。因为 \(101 > 81\),贪心算法选择切断分支 A。后果:虽然保住了 101 个节点,但分支 B 的病毒立刻爆发,下一秒瞬间感染了 80 个节点,且因为层数少,很快就没得切了。
最优解(Human Logic):应该切断分支 B。虽然 \(siz_v\) 只有 81,但它下一秒会造成毁灭性打击。切掉 B 后,分支 A 虽然长,但它每一秒只传染 1 个人。我们有充足的时间在接下来的几秒内慢慢去处理分支 A 的后续节点(甚至如果你手速够快,可以一层层切下去)。

解法:暴搜 + 剪枝

void solve(){
    int n, p;
    cin >> n >> p;
    vector e(n + 1, vector<int>());
    vector<int> sz(n + 1), dep(n + 1);
    for(int i = 0; i < p; ++ i){
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    auto get_sz_dep = [&](auto&& self, int u, int fa) -> void {
        sz[u] = 1;
        dep[u] = dep[fa] + 1;
        for(auto v : e[u]){
            if(v == fa) continue;
            self(self, v, u);
            sz[u] += sz[v];
        }
        return ;
    };
    get_sz_dep(get_sz_dep, 1, 0);
    vector<int> ill(n + 1);
    ill[1] = 1;
    int ans = inf;
    auto dfs = [&](auto&& self, int d, int cnt) -> void {
        if(cnt >= ans) return ;
        bool flag = true;
        for(int i = 1; i <= n; ++ i){
            if(d == dep[i] && ill[i]){
                for(auto v : e[i]){
                    if(dep[v] < dep[i]) continue;
                    flag = false;
                    ill[v] = 1;
                    ++ cnt;
                }
            }
        }
        -- cnt;
        for(int i = 1; i <= n; ++ i){ // 枚举裁剪点
            if(dep[i] == d + 1 && ill[i]){
                ill[i] = 0;
                self(self, d + 1, cnt);
                ill[i] = 1;
            }
        }
        ++ cnt;
        for(int i = 1; i <= n; ++ i){
            if(d == dep[i] && ill[i]){
                for(auto v : e[i]){
                    if(dep[v] < dep[i]) continue;
                    ill[v] = 0;
                    -- cnt;
                }
            }
        }
        if(flag){
            ans = min(ans, cnt);
        }
    };
    dfs(dfs, 1, 1);
    cout << ans << "\n";
    return ;
}

Problem J. 邮票面值设计

原题 link:https://www.luogu.com.cn/problem/P1021
题解 link:https://www.luogu.com.cn/article/fnxn3dgc

搜索 + 剪枝

void solve(){
    int n, m;
    cin >> n >> m;
    vector<int> a(m + 1), ans(m + 1), dp(N);
    auto calc = [&](int k) -> int {
        fill(dp.begin(), dp.end(), inf);
        dp[0] = 0;
        for(int i = 1; i <= k; ++ i){ // 前 k 个数
            for(int j = a[i]; j <= a[k] * n; ++ j){
                if(dp[j - a[i]] < n)
                    dp[j] = min(dp[j], dp[j - a[i]] + 1);
            }
        }
        int x = 0;
        while(dp[x + 1] < inf) ++ x;
        return x;
    };
    int mx = 0;
    auto dfs = [&](auto&& self, int u) -> void {
        if(u == m + 1){
            int t = calc(u - 1);
            if(t > mx){
                mx = t;
                ans = a;
            }
            return ;
        }
        int end = calc(u - 1);
        for(int i = a[u - 1] + 1; i <= end + 1; ++ i){
            a[u] = i;
            self(self, u + 1);
            a[u] = 0;
        }
        return ;
    };
    a[1] = 1;
    dfs(dfs, 2);
    for(int i = 1; i <= m; ++ i){
        cout << ans[i] << " \n"[i == m];
    }
    cout << "MAX=" << mx << '\n';
    return ;
}
posted @ 2025-09-21 23:18  Qiansui  阅读(6)  评论(0)    收藏  举报