1. 程式人生 > 實用技巧 >一些很妙的思維題

一些很妙的思維題

一些很妙的思維題

P4643 [國家集訓隊]阿狸和桃子的遊戲

\(Describe\)

有一張 \(n\) 個點 \(m\) 條邊的圖,點有點權,邊有邊權。

先手後手輪流染黑白兩色,最後的得分是自己染的點權和 + 兩端均為自己的顏色的邊權和。

雙方都希望自己的得分 - 對手的得分最大,求結果。

\(1 \le n \le 10000, 0 \le m \le 100000\)

\(Solution\)

這題巨妙無比,真是妙蛙種子吃著妙脆角,秒進了米奇妙妙屋,秒到家了。

你可以考慮把每一條邊都分到他所連的 \(2\) 個點上,各一半。

然後對於每條邊,我們有 \(2\) 種可能,如下:

  • \(2\) 邊都是 \(A\)

    取的,那麼 \(2\) 個點之和剛好是貢獻。

  • 如果 \(2\) 邊不一樣,那麼一邊都是一半,相互抵消。

\(Code\)

#include <iostream>
#include <algorithm>
#include <stdio.h>
using namespace std;
const int maxn = 10010;
int n, m, u, v, w;
double a[maxn];
double cmp(double x, double y) {
    return x >= y;
}
int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++ i) {
        scanf("%lf", &a[i]);
    }
    for(int i = 1; i <= m; ++ i) {
        scanf("%d%d%d", &u, &v, &w);
        a[u] += (double) w / 2;
        a[v] += (double) w / 2;
    }
    sort(a + 1, a + n + 1, cmp);
    double ans = 0;
    for(int i = 1; i <= n; ++ i) {
        if(i % 2) {
            ans += a[i];
        }
        else {
            ans -= a[i];
        }
    }
    printf("%d", (int) ans);
    return 0;
}

\(Mushroom\) 的區間

\(Describe\)

給你若干個區間 \([l_i, r_i]\) ,每次可以對一個區間進行整體異或操作。

區間初始全 \(0\),問你最後可以形成多少種不同的區間。

\(Solution\)

考慮什麼樣的區間是沒有貢獻的。

假設對於區間 \([l, r]\) ,我們考慮如果存在以下三個區間,那麼這個區間是無用的。

  • [i, l - 1]

  • [r + 1, j]

  • [i, j]

那麼通過不同的組合,有無區間 \([l, r]\) 是沒有意義的。

那麼用並查集維護端點就行了。

\(Code\)

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100010;
const int XRZ = 1e9 + 7;
const int maxm = (1 << 21);
int n, m, ans = 1, l, r, f[maxn];
int find(int x) {
	return f[x] == x ? x : f[x] = find(f[x]);
}
int main() {
	freopen("section.in", "r", stdin);
	freopen("section.out", "w", stdout);
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; ++ i) {
		f[i] = i;
	}
	for(int i = 1; i <= m; ++ i) {
		scanf("%d%d", &l, &r);
		int x = find(l), y = find(r + 1);
		if(x == y) {
			continue;
		}
		f[x] = y;
		ans = (ans * 2) % XRZ;
	}
	printf("%d", ans);
	return 0;
}


P3545 [POI2012]HUR-Warehouse Store

\(Describe\)

一共 \(n\) 天,每天上午會進 \(A_i\) 的物品,中午會有一個客人想要買走 \(B_i\) 的物品,當然你也可以選擇不買,問你最後最多可以交易多少次。

資料範圍 : \(1 <= n <= 250000, 0 <= A_i, B_i <= 10^9\)

\(Solution\)

首先,我們看到題目先會想到 \(01\) 揹包。是個正確的作法,但是肯定過不了。

然後,我們考慮到這一定是個貪心/推式子的題目。

但是推式子的題目一般的特性在這裡顯然不符合(推式子的題目一般不會讓你選擇)

那接下來考慮怎麼去貪心。

第一想法是我們選最小的肯定是最優的,因為就算選了這個最小的而導致其他的不可以選,那也最多隻能導致一個不可以選。

因為如果可以導致多個不可以選的話,選了次小的哪一個也會導致更多的不可以選。

所以選擇最小的策略是對的,\(sort\) 一般的複雜度我們也可以接受,但是問題來了,我們那些數可以取?

取完當前最小的,那麼它前面的一些數說不定不可以再去。

那麼處理這些的複雜度就不穩定了,可以構造資料卡掉。

接下來我們想每天上午進的貨物會對什麼有影響。

這個很顯然,它可以對他後面的所有數產生影響,因為到貨了就可以賣給別人了。

那是不是可以這麼考慮,從後往前去跑,每次去當前可以取的最小的。

因為從後完全就去除了它的後效性,所有正確性也是對的。

但是新增一個數 \(A_i\) 可能很大,一次可以處理多個數。

那你就必須保證後面整個序列是有序的,而且還要維護一個字尾和。

我們考慮維護這個有序數列的複雜度最優,那就是往前加一個數,就把這個數加入到數列裡面去。

這樣的複雜度最劣是 \(O(n^2)\) 的,還是會炸。

那我們是不是無法從一些順序上去找到優化的空間。

我們就可以考慮去莽,然後支援撤銷,這樣也可以保證正確性。

具體的話是這樣實現的:

從前往後讀入,每次可以取就取,並且把這些取過的記錄在單調佇列中。

如果當前這個放不下,我們就把他和佇列首的元素做比較,不斷更新即可。

根據我們的第二個想法,每個前面取過的如果彈出來,因為他的後效性,肯定是可以服務後面的。

那麼就結束了。

\(Code\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 250010;
int n, sum, ans, book[maxn];
priority_queue<pair<int, int>, vector<pair<int, int> > > Q;
struct node {
    int a, b, id;
} e[maxn];
int cmp(node x, node y) {
    return x.b < y.b;
}
signed main() {
    scanf("%lld", &n);
    for(int i = 1; i <= n; ++ i) {
        scanf("%lld", &e[i].a);
        e[i].id = i;
    }
    for(int i = 1; i <= n; ++ i) {
        scanf("%lld", &e[i].b);
    }
    for(int i = 1; i <= n; ++ i) {
        sum += e[i].a;
        if(e[i].b < sum) {
            Q.push(make_pair(e[i].b, e[i].id));
            ++ ans;
            book[i] = 1;
            sum -= e[i].b;
        }
        else if(! Q.empty() && Q.top().first > e[i].b) {
            book[Q.top().second] = 0;
            book[i] = 1;
            sum += e[Q.top().second].b - e[i].b;
            Q.pop(); Q.push(make_pair(e[i].b, e[i].id));
        }
    }
    printf("%lld\n", ans);
    for(int i = 1; i <= n; ++ i) {
        if(book[i]) {
            printf("%d ", i);
        }
    }
    return 0;
}