同余最短路の学习
前言
好困的下午,反正没精神打题不如总结总结知识比较好。
正文
说白了其实就是最短路,不过在点和边的设计上很巧妙,多说理论实在枯燥,不如结合题目来说。
墨墨的等式
题意
给出 \(a_{1\dots n}\) , \(l\), \(r\)。
求出 \([l,r]\) 中可以使 \(\sum^{n}_{i=1}{a_i x_i} = b\) 有非负整数解的 \(b\) 的个数。
其实容易想到多半就是求出小于等于 \(r\) 和 小于 \(l\) 的部分容斥出答案,问题便来到了如何求出这样的部分,也便用到了所谓同余最短路。
先考虑 \(n\) 比较小的情况,比如 \(3\),则有 \(a_1 x_1 + a_2 x_2 + a_3 x_3 = b\),考虑这样子的式子的解的特点。注意到当等式两边同时对任意已有的 \(a_i\) 取模,则等式右侧结果不会因为 \(x_i\) 的改变而改变,这也就是建立了一个小小的模型。$$a_1 x_1 + a_2 x_2 + a_3 x_3 \mod a_1 = a_2 x_2 + a_3 x_3 = b \mod a_1$$ 考虑怎么把这个式子做成我们想要的可以拿来转移从而求出 \(b\) 的个数的东西,不妨继续向等式两边加入一些数,像这样:
容易得出只有发生了 \(x_{i\&\&i!=1}\) 的变化才会导致结果的变化,容易得到在没有超过 \(r\) 的上界范围内的每一个数 \(y\),都可以列出这样的转移式 \(y\mod a_1 -> (y + a_i) \mod a_1 \space\space cost = a_i\) 注意到这个式子显然可以当做最短路的转移,可以由此建出一个图来,在图上跑最短路,令 \(dis_{i}\) 为从 \(0\)(显然 \(dis_{0} = 0\))到 \(i\) 的最短路,这所谓最短路也就代表了令等号右边成为 \(i\) 消耗的最小代价,而 \((r - dis_i)\)(或\((l - dis_i)\)) 也就代表了剩余的可操作空间,让 \(dis_i\) 不变显然这一空间只能够塞 \(a_1\),能塞多少个也就是在 \(b % a_1 = dis_i\) 条件下解的个数,统计后作差即可。
顺带一提这种最短路很难卡死 spfa (不是不能,只是很难),兴许可以算是 spfa 的一次复活(?)
code:
#include <bits/extc++.h>
#define submit
#ifdef submit
#define getchar getchar_unlocked
#define putchar putchar_unlocked
#endif
#define e_ putchar(' ')
#define en_ putchar('\n')
using namespace std;
template <typename T> inline T in(T &n) {
n = 0; char p = getchar();
while(p < '-') p = getchar();
bool f = p == '-' ? p = getchar() : 0;
do n = (n << 1) + (n << 3) + (p ^ 48), p = getchar();
while(isdigit(p));
return n = f ? -n : n;
return n;
}
template <typename T> inline void out(T x) {
if(x < 0) putchar('-'), x = -x;
if (x >= 10) out(x / 10);
putchar(x % 10 + '0');
}
constexpr int N = 500000 + 10; const char me[] = "Ciallo~(∠・ω< )⌒ ★ ";
int a[14], n, l, r, dis[N], x = INT_MAX;
struct node {
int to, w, nxt;
} e[N * 12];
int hd[N], tot;
inline void add(int u, int v, int w) {
e[++tot] = {v, w, hd[u]};
hd[u] = tot;
}
bool vis[N];
void dij(int s) {
memset(dis, 63, (x + 2) * 8); // 科学的 memset 更好的效率!!! 强推!!!
dis[s] = 0;
priority_queue<pair<int, int> > q;
q.push({0, s});
int u;
while(!q.empty()) {
u = q.top().second; q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(int i = hd[u], v = e[i].to, w = e[i].w; i; i = e[i].nxt, v = e[i].to, w = e[i].w)
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push({-dis[v], v});
}
}
}
struct {
void excute(const char *I) {
in(n), in(l), in(r);
for(int i = 1; i <= n; i ++) { in(a[i]); if(a[i]) x = min(a[i], x);}// 注意虽然小点更好,但也不能太小(为0),那不合法
for(int i = 0 ; i < x; i ++) {
for(int j = 1; j <= n; j ++) {
if(a[j] != x)
add(i, (a[j] + i) % x, a[j]); // 建图
}
}
dij(0);
int ans = 0;
for(int i = 0; i < x; i ++) {
if(dis[i] <= r) ans += (r - dis[i]) / x + 1;
if(dis[i] < l) ans -= (l - 1 - dis[i]) / x + 1;
}
cout << ans << I;
}
} world;
signed main() {
world.excute(me);
}
[arc084_b]Small Multiple
题意
把这个拿出来是因为有一点点特殊,先把特殊点说了,这题转化完了可以是一个 01bfs 而并不止是 dij 或 spfa 之类的(再大点的数据范围也许可以卡掉最短路?)。然后咱来好好聊聊。
(如果你真的只看了我一个人上面说的东西的话)一个很难想到的点就是哪里来的同余?事实是这样的,我们不妨考虑一个数字的由来,如 \(114\),它是由 "1个100,1个10,4个1" 组成的数字,细想一下,也就是它主要由两种操作 —— \(n+1\) 和 \(n*10\) 构成,也就是说,在不考虑进位的情况下,对于任意一个数字 \(x\) 都有着两种转移,\(x -> x+1 \space\space cost = 1\)、\(x -> x*10 \space\space cost = 0\)、考虑 01bfs 利用双端队列始终维护队头的 \(cost_{sum}\) 为最小的,直到出现了一个 \(\mod k == 0\) 的数字就输出。注意到空间框框爆炸,又注意到在 \(\mod k\) 意义下得数不变,所以可以考虑使用高贵的 \(vis\) 数组解决这一切。
code:
#include <bits/extc++.h>
//#define submit
#ifdef submit
#define getchar getchar_unlocked
#define putchar putchar_unlocked
#endif
#define e_ putchar(' ')
#define en_ putchar('\n')
using namespace std;
template <typename T> inline T in(T &n) {
n = 0; char p = getchar();
while(p < '-') p = getchar();
bool f = p == '-' ? p = getchar() : 0;
do n = (n << 1) + (n << 3) + (p ^ 48), p = getchar();
while(isdigit(p));
return n = f ? -n : n;
return n;
}
template <typename T> inline void out(T x) {
if(x < 0) putchar('-'), x = -x;
if (x >= 10) out(x / 10);
putchar(x % 10 + '0');
}
constexpr int N = 1e6 + 10; const char me[] = "私の0721を見てください!";
typedef pair<double, int> node;
using lint = long long;
class {
public:
int k, ans, vis[N];
deque<node> q;
inline void excute(const char *I) {
in(k);
q.push_back({1, 1});
vis[1] = 1;
while(!q.empty()) {
int u = q.front().first, w = q.front().second;
q.pop_front();
if(u == 0) {
out(w); en_;
return;
}
if(!vis[10 * u % k]) {
q.push_front({10 * u % k, w});
vis[10 * u % k] = 1;
}
if(!vis[u + 1]) {
q.push_back({u + 1, w + 1});
}
}
}
} world;
signed main() {
world.excute(me);
}
总之先说到这里吧,更高深的应用我讲起来就不太好了(主要是懒),其实还有一个名为转圈的技巧想说,不过还是咕咕罢。。。。。

浙公网安备 33010602011771号