1. 程式人生 > 實用技巧 >「失戀三部曲」那一天她與我許下約定 + 那一天她離我而去 + 哪一天她能重回我身邊

「失戀三部曲」那一天她與我許下約定 + 那一天她離我而去 + 哪一天她能重回我身邊

那一天她與我許下約定

那一天我們在教室裡許下約定。
我至今還記得我們許下約定時的歡聲笑語。我記得她說過她喜歡吃餅乾,很在意自己體重的同時又控制不住自己。她跟我做好了約定:我拿走她所有的餅乾共 \(N\) 塊,在從今天起不超過 \(D\) 天的時間裡把所有的餅乾分次給她,每天給她的餅乾數要少於 \(M\) 以防止她吃太多。
當然,我們的約定並不是餅乾的約定,而是一些不可言狀之物。
現今回想這些,我突然想知道,有多少種方案來把餅乾分給我的她。

\(N,M \leq 2000, D \leq 10^{12}\)

首先,定義\(dp[i][j]\)為到第i天,分了j塊餅乾的方案數。這樣可以直接寫出一個 \(O(NMD)\)

的樸素演算法。

正解是優化這個暴力,因為 \(D\) 遠大於 \(N\)\(M\) ,所以直接DP會有很多一個餅乾都不給的無用轉移。

因此我們規定每天至少都給一塊餅乾,算出方案數後再分配到\(D\)天裡即可。

轉移:

\[dp[i][j]=\sum_{k=max(j-m+1, \ 1)}^{j-1}dp[i][k] \]

這個轉移可以用對dp陣列做字首和來優化

統計答案:

\[ans = \sum_{i = 1}^{n}dp[i][n] \times {d \choose n} \]

還有一個問題是組合數怎麼算,是因為\(D\)巨大,組合數用階乘會爆炸,遞推又太慢,考慮優化一下。

\[{d \choose i} = \frac{d!}{i! \times (d-i)!} = \frac{A_{d}^{i}}{i!} \]

\(A_{d}^{i}\)是可以線性處理的,再乘上\(i!\)的逆元就得到了相應的\(d \choose i\)

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 2e3 + 10;
const int p = 998244353;

long long Qpow(long long x, long long t) {
    long long s = 1;
    while (t) {
	if (t & 1) s = s * x % p;
	x = x * x % p;
	t >>= 1;
    }
    return s;
}

int jc[maxn], inv[maxn];
int A[maxn], c[maxn];

long long n, m;
long long d;
long long f[maxn][maxn], sum[maxn][maxn];

int main() {
    //freopen("contract.in", "r", stdin);
    //freopen("contract.out", "w", stdout);
    jc[0] = 1;
    for (int i = 1; i <= 2000; i++) jc[i] = 1LL * i * jc[i  - 1] % p;
    inv[2000] = Qpow(1LL * jc[2000], p - 2);
    for (int i = 2000; i; i--) inv[i - 1] = 1LL * i * inv[i] % p;
    inv[0] = 1;

    while (~scanf("%lld %lld %lld", &n, &d, &m)) {
	if (!n && !d && !m) break;
	if (n > d * (m - 1)) {
	    printf("0\n");
	    continue;
	}
	memset(f, 0, sizeof f);
	memset(sum, 0, sizeof sum);
	A[0] = 1; 
	for (int i = 1; i <= n; i++) {
	    A[i] = 1LL * (d - i + 1 + p) % p * A[i - 1] % p;
	    c[i] = 1LL * A[i] * inv[i] % p;
	}
	for (int i = 1; i < m; i++) f[1][i] = 1, sum[1][i] = f[1][i] + sum[1][i - 1];
	for (int i = m; i <= n; i++) sum[1][i] = sum[1][i - 1];
	long long ans = f[1][n] * c[1] % p;
	for (int i = 2; i <= min((long long)n, d); i++) {
	    for (int j = i; j <= n; j++) {
		f[i][j] = (sum[i - 1][j - 1] - sum[i - 1][max((long long)(j - m), 0LL)]) % p;
		sum[i][j] = (sum[i][j - 1] + f[i][j]) % p;
	    }
	}
	for (int i = 2; i <= min((long long)n, d); i++) {
	    ans = (ans + f[i][n] * c[i] % p) % p;
	}
	printf("%lld\n", ans);
    }
}

那一天她離我而去

她走的悄無聲息,消失的無影無蹤。
至今我還記得那一段時間,我們一起旅遊,一起遊遍山水。到了最終的景點,她卻悄無聲息地消失了,只剩我孤身而返。
現在我還記得,那個旅遊區可以表示為一張由 \(n\) 個節點 \(m\) 條邊組成無向圖。我故地重遊,卻發現自己只想儘快地結束這次旅遊。我從景區的出發點(即 \(1\) 號節點)出發,卻只想找出最短的一條迴路重新回到出發點,並且中途不重複經過任意一條邊。
即:我想找出從出發點到出發點的小環。

\(T\) 組資料,\(T \leq 10, n \leq 10^4, m \leq 4 \times 10^4\) , 邊長 \(\leq 10^3\),保證不存在重邊,自環

很奇(e)怪(x)的一道題...

放一個優化爆搜,即先用Dij預處理出到1到每個點的最短距離,搜尋時發現當前距離加上回去的最短距離(實際上是可能沒法走這個距離的,因為不能走重)已經大於當前的答案,就直接回溯。

這個碼在上屆學長那是能A的...但現在被卡成了92分,因為加了一個菊花圖...

#include <cstdio>
#include <queue>
#include <iostream>
#include <algorithm>
#include <ctime>
#include <cstring>
using namespace std;
char buf[1 << 20], *p1 = buf, *p2 = buf;
char getc() {
    if (p1 == p2) {
	p1 = buf, p2 = buf + fread(buf, 1, 1 << 20, stdin);
	if (p1 == p2) return EOF;
    }    
    return *p1++;
}
inline int read() {
    int s = 0, w = 1;
    char c = getc();
    while (c < '0' || c > '9') { if (c == '-') w = -1; c = getc(); }
    while (c >= '0' && c <= '9') s = s * 10 + c - '0', c = getc();
    return s * w;
}
const int maxn = 1e4 + 10;
const int maxm = 4e4 + 10;
struct edge {
    int nex, to, w;
    edge() {}
    edge(int a, int b, int c) {
	nex = a, to = b, w = c;
    }
}e[maxm << 1];
int head[maxn], tot;
void Add(int u, int v, int w) {
    e[++tot] = edge(head[u], v, w);
    head[u] = tot;
}
long long ans;
int d[maxn];
bool vis[maxm << 1];
struct node {
    int dis, id;
    node() {}
    node(int a, int b) {
	id = a, dis = b;
    }
    bool operator < (const node& x) const {
	return dis > x.dis;
    }
};
priority_queue<node> q;
void Dij(int s) {
    memset(d, 0x3f, sizeof d);
    memset(vis, 0, sizeof vis);
    d[s] = 0;
    q.push(node(s, d[s]));
    while (!q.empty()) {
	int u = q.top().id;
	q.pop();
	if (vis[u]) continue;
	vis[u] = 1;
	for (int i = head[u]; i; i = e[i].nex) {
	    int v = e[i].to;
	    if (d[v] > d[u] + e[i].w) {
		d[v] = d[u] + e[i].w;
		q.push(node(v, d[v]));
	    }
	}
    }
}
void DFS(int u, long long dis, bool flag) {
    if (dis + d[u] >= ans) return;
    if (u == 1 && flag) {
	ans = min(ans, dis);
	return;
    }
    for (int i = head[u]; i; i = e[i].nex) {
	if (vis[i]) continue;
	int v = e[i].to;
	if (i % 2 == 1)
	    vis[i] = vis[i + 1] = 1;
	else 
	    vis[i] = vis[i - 1] = 1;
	if (v == 1)
	    DFS(v, dis + e[i].w, 1);
	else 
	    DFS(v, dis + e[i].w, 0);
	if (i % 2 == 1) 
	    vis[i] = vis[i + 1] = 0;
	else 
	    vis[i] = vis[i - 1] = 0;
    }
}
int main() {
    //freopen("leave.in", "r", stdin);
    //freopen("leave.out", "w", stdout);
    int t = read();
    int n, m, u, v, w;
    st = clock();
    while (t--) {
	ans = 9999999999999;
	n = read(), m = read();
	for (int i = 1; i <= m; i++) {
	    u = read(), v = read(), w = read();
	    Add(u, v, w), Add(v, u, w);
	}
	Dij(1);
	memset(vis, 0, sizeof vis);
	DFS(1, 0, 0);
	if (ans != 9999999999999)
		printf("%lld\n", ans);
	else 
		printf("-1\n");
	memset(head, 0, sizeof(head));
	tot = 0;
    }
    return 0;
}

正解是對點的編號二進位制拆分,列舉每個二進位制位,把點的編號在該二進位制位上相同的所有點作為起點,然後搜尋,然後這樣一定是能把所有的情況都算到的...

然後我很不理解這種做法的複雜度,也沒寫...咕了

哪一天她能重回我身邊

她依然在我不知道的地方做我不知道的事。
桌面上攤開著一些卡牌,這是她平時很愛玩的一個遊戲。如今卡牌還在,她卻不在我身邊。不知不覺,我翻開了卡牌,回憶起了當時一起玩卡牌的那段時間。
每張卡牌的正面與反面都各有一個數字,我每次把卡牌按照我想的放到桌子上,一共 \(n\) 張,而她則是將其中的一些卡牌翻轉,最後使得桌面上所有朝上的數字都各不相同。
我望著自己不知不覺翻開的卡牌,突然想起了之前她曾不止一次的讓我幫她計算最少達成目標所需要的最少的翻轉次數,以及最少翻轉達成目標的方案數。(兩種方式被認為是相同的當且僅當兩種方式需要翻轉的卡牌的集合相同)
如果我把求解過程寫成程式發給她,她以後玩這個遊戲的時候會不會更順心一些?

\(T\)組資料,\(T \leq 50\)\(n \leq 10^5\)

ex到了,咱還不會呢,先咕了。