「失恋三部曲」那一天她与我许下约定 + 那一天她离我而去 + 哪一天她能重回我身边

那一天她与我许下约定

那一天我们在教室里许下约定。
我至今还记得我们许下约定时的欢声笑语。我记得她说过她喜欢吃饼干,很在意自己体重的同时又控制不住自己。她跟我做好了约定:我拿走她所有的饼干共 \(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\) 张,而她则是将其中的一些卡牌翻转,最后使得桌面上所有朝上的数字都各不相同。
我望着自己不知不觉翻开的卡牌,突然想起了之前她曾不止一次的让我帮她计算最少达成目标所需要的最少的翻转次数,以及最少翻转达成目标的方案数。(两种方式被认为是相同的当且仅当两种方式需要翻转的卡牌的集合相同)
如果我把求解过程写成程序发给她,她以后玩这个游戏的时候会不会更顺心一些?

多组数据
\(20\)% \(T=1 \, , n \leq 20\)
\(100\)%,\(T \leq 50\)\(n \leq 10^5\)

20%做法当然随便搞。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1048576 + 10;
int a[50], flag[60];
int x[40], y[40];
int main() {
    freopen("back.in", "r", stdin);
    freopen("back.out", "w", stdout);
    int t;
    cin >> t;
    while (t--) {
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) {
	    scanf("%d %d", &x[i], &y[i]);
	}
	int ans = INT_MAX, anscnt = 0;
	for (int s = (1 << n) - 1; s >= 0; --s) {
	    memset(a, 0, sizeof a);
	    memset(flag, 0, sizeof flag);

	    int cnt = 0;
	    for (int i = 1; i <= n; ++i) {
		if (s & (1 << (i - 1))) {
		    a[i] = y[i];
		    cnt++;
		}
		else a[i] = x[i];
	    }
	    for (int i = 1; i <= n; ++i) {
		if (flag[a[i]]) break;
		flag[a[i]] = 1;
		if (i == n) {
		    if (ans > cnt) ans = cnt, anscnt = 1;
		    else if (ans == cnt) anscnt++;
		}
	    }
	}
	if (ans == INT_MAX) {
	    printf("-1 -1\n");
	    continue;
	}
	printf("%d %d\n", ans, anscnt);
    }
    return 0;
}

然后这道题的正解真的是图论,但不是二分图,2-sat之类的东西。
因为内存只有64M,所以2-sat可以否掉,而且正确性的判定也是个问题。
再然后题解给了一种很巧妙的建图方式,即从每张卡片的正面数字向反面数字建边。这样你会发现只有当每个点的出度都小于等于1时,才是一个合法解。
翻转的操作就相当于将边的方向掉转,考虑到实现边的反向比较麻烦,考虑建双向边,用边权来代表边的方向,1代表是u指向v的,0代表是v指向u的。
这样建图之后,你再经过一番思考,会发现对于每一个联通块,合法的情况只会是树、基环树两种情况。
至于证明的话,咱也不会证。不过你会发现无论怎么构造,都是无法让一张非树图里的所有点的出度都小于等于1的。
树的话,要先考虑链,一条链的合法情况就是让所有边都统一地指向某一个节点,所以在树上就是让所有边都统一地指向根方向的父亲即可。根是需要枚举的,所以想到了换根DP。

\(g[u]\)为以u为根时所需要的最小反转次数,则当一条边是由u指向v时,\(g[v] = g[u]-1\),反之则是\(g[v] = g[u]+1\)
对于每个联通块,找到最小的\(g[u]\),这是这个块的最小次数,统计这个最小的\(g[u]\)一共有多少个,这是这个块的方案数。
总的最小次数就是所有块最小次数相加,方案数就是所有块方案数相乘。

然后基环树的情况就是把那个环看成一个大根,这个环要么是顺时针,要么是逆时针,这样环上每个点出度都为1,再让其他点都指向环即可。
做法就是找环,断一条边,按普通树的做法做一遍。设from,to为断掉这条边的两个端点。则当断掉这条边是指向from时,g[to]++,指向to时,g[to]++。
如果g[from] == g[to],则方案数为2,否则为1,最小次数就是两者中的最小值。

另外,注意判掉自环的情况。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int maxn = 2e5 + 10;
const int mod = 998244353;
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;
}
struct EDGE { 
	int nex[maxn << 1];
	int to[maxn << 1];
	int w[maxn << 1];
	int head[maxn], tot;
	void Init() {
		memset(head, 0, sizeof head);
		tot = 1;
	}
	void Add(int u, int v, int val) {
		nex[++tot] = head[u];
		to[tot] = v;
		w[tot] = val;
		head[u] = tot;
	}
} edge;

int n, dian_cnt, edge_cnt, edge_flag, from, to, root;
int g[maxn], f[maxn];
bool checked[maxn], vis[maxn], edge_vis[maxn];
vector<int> ans;

void Dfs_check(int u, int fa) {
	vis[u] = 1;
	dian_cnt++;
	for (int i = edge.head[u]; i; i = edge.nex[i]) {
		edge_cnt++;
		int v = edge.to[i];
		if (v == fa || vis[v]) continue;
		Dfs_check(v, u);
	}	
}

void Dfs_pre(int u, int fa) {
	vis[u] = 1, f[u] = 0;
	for (int i = edge.head[u]; i; i = edge.nex[i]) {
		int v = edge.to[i];
		if (v == fa) continue;
		if (vis[v]) {
			from = u;
			to = v;
			edge_flag = i;
		}
		else {
			Dfs_pre(v, u);
			f[u] += f[v] + edge.w[i];
		}
	}
}

void Dfs_work(int u, int fa) {
	ans.push_back(g[u]);
	for (int i = edge.head[u]; i; i = edge.nex[i]) {
		int v = edge.to[i];
		if (v == fa || i == edge_flag || i == (edge_flag ^ 1)) continue;
		if (edge.w[i]) g[v] = g[u] - 1;
		else g[v] = g[u] + 1;
		Dfs_work(v, u);
	}
}

bool Check() {
	for (int i = 1; i <= n * 2; ++i) {
		if (vis[i]) continue;
		edge_cnt = dian_cnt = 0;
		Dfs_check(i, 0);
		if (dian_cnt * 2 < edge_cnt) return 0; //双向边,所以点数乘2
	}
	return 1;
}

void Work() {
	n = read();
	edge.Init();
	memset(g, 0, sizeof g);
	memset(f, 0, sizeof f);
	int x, y;
	for (int i = 1; i <= n; ++i) {
		x = read(), y = read();
		edge.Add(x, y, 1);
		edge.Add(y, x, 0);
	}
	memset(vis, 0, sizeof vis);
	if (!Check()) return printf("-1 -1\n"), void();
	memset(vis, 0, sizeof vis);
	int res, mi = 0, cnt = 1;
	for (register int i = 1; i <= n * 2; ++i) {
		if (vis[i]) continue;
		from = to = edge_flag = -1;
		res = 0;
		ans.clear();
		Dfs_pre(i, 0);
		g[i] = f[i];
		Dfs_work(i, 0);
		if (edge_flag == -1) {
			sort(ans.begin(), ans.end());
			for (int i = 0; i < ans.size(); ++i) {
				if (ans[i] == ans[0]) res++;
				else break;
			}
			mi += ans[0];
		}
		else {
			if (from == to) { //注意判掉自环
				res = 1;
				mi += min(g[from], g[to]);
			}
			else {
				if (!edge.w[edge_flag]) edge_flag ^= 1;
				if (edge.to[edge_flag] == to) g[to]++; //边所指向的那个点的翻转次数要+1
				else g[from]++;
				if (g[from] == g[to]) res = 2;
				else res = 1;
				mi += min(g[from], g[to]);
			}
		}
		cnt = 1LL * cnt * res % mod;
	}
	printf("%d %d\n", mi, cnt);
}

int main() {
	freopen("back.in", "r", stdin);
	freopen("back.out", "w", stdout);
	int t = read();
	while (t--) {
		Work();
	}
}
posted @ 2020-10-09 14:31  zfio  阅读(370)  评论(1编辑  收藏  举报