A 貿易
題目保證輸入的邊 \(u < v\),說明題目中的圖是一個有向無環圖 \(DAG\),但是不一定連通。可以記錄 \(f[i]\) 表示到達 \(i\) 之前能遇到的最小的價格,使用拓撲排序進行 \(dp\) 轉移。對於每一個點 \(i\) , 如果其價格為 \(a[i]\), 就可以用 \(a[i] - f[i]\) 更新答案,取最大值即可,複雜度 \(O(n+m)\)。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
vector<int> v[maxn];
int du[maxn], n, m;
int val[maxn], f[maxn];
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> val[i], f[i] = 2e9 + 7;
for(int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
v[x].push_back(y);
du[y]++;
}
queue<int> q;
for(int i = 1; i <= n; i++) if(!du[i]) q.push(i);
int ans = -2e9 - 7;
while(!q.empty()) {
int now = q.front(); q.pop();
ans = max(ans, val[now] - f[now]);
f[now] = min(f[now], val[now]);
for(int i = 0; i < v[now].size(); i++) {
int to = v[now][i];
f[to] = min(f[to], f[now]);
du[to]--;
if(!du[to]) q.push(to);
}
}
cout << ans;
return 0;
}
B 格子游戲
因為每次操作會影響右邊和下邊,因此根據貪心,最優的一定是從左上到右下操作,這樣已經變成 \(0\) 的格子不會再改變。容易發現,這樣的策略下,每個點的操作次數等於它上面和左面操作次數的和,因此這是一個楊輝三角。接下來就簡單了,根據楊輝三角第 \(i\) 行第 \(j\) 列的值是 \(C_{i-1}^{j-1}\),對應到這個傾斜的楊輝三角上就是 \(C_{i-1}^{i+j-2}\)。
所以對於每一行,產生的答案就是楊輝三角第 \(i+1\) 行前 \(a_i+1\) 個數的和,而這個值就是第 \(i+1\) 行第 \(a_i\) 列的值。因此答案為 \(\sum_{i=0}^n C_{i+a_i}^i\)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
const int mod = 1e9 + 7;
long long ksm(int x, int y) {
int res = 1;
while(y) {
if(y & 1) res = 1LL * res * x % mod;
x = 1LL * x * x % mod;
y >>= 1;
}
return res;
}
int mul[maxn << 1];
int calc(int x, int y) {
return 1LL * mul[x] * ksm(mul[y], mod - 2) % mod * ksm(mul[x - y], mod - 2) % mod;
}
int a[maxn];
int main() {
int n;
cin >> n;
for(int i = 1; i <= n + 1; i++) cin >> a[i];
mul[0] = 1;
for(int i = 1; i < (maxn << 1); i++) mul[i] = 1LL * mul[i - 1] * i % mod;
int ans = 0;
for(int i = 1; i <= n + 1; i++) (ans += calc(i + a[i] - 1, i)) %= mod;
printf("%d", ans);
return 0;
}
C \(ST\) 距離
首先,考慮如何計算兩個串的答案。 首先把奇數位置的值取反,那每次操作相當於 \(01\rightarrow 10\) 或 \(10\rightarrow 01\)。於是當兩個串 \(1\) 的個數相等時可以達成。 可以看作若干個 \(1\) 在一條鏈上移動到新的位置。答案為距離之和,把移動貢獻均攤到每條邊上,那每一條邊的貢獻就是 兩個串 \([1,i]\) 中的 \(1\) 個數的差值。 列舉 \([1,i]\) 中兩個串 \(?\) 處填寫 \(0\) 的差值個數,那另一邊 \([i+1,n]\) 中 ? 處填寫 \(0\) 的差值個數確定。 如果確定了一邊的 \(?\) 處填寫 \(0\) 的差值個數,那列舉第一個串中的 0 個數,一邊的答案形如:
可以用範德蒙德卷積,化成一個組合數:
另一邊也相同,方案數就是兩邊乘起來。於是可以得到 \(O(n^2)\) 的做法。 觀察這個式子,發現我們每次要求的形如:
要求這個答案 \(n\) 次,並且每次的 \(A+B,A_2+B_2\) 為定值。 這個式子很像範德蒙德卷積,考慮把絕對值先拆成 \(|j|=(-j)+(2j)[0\leq j]\)。第一部分要求 \(C_A^{A_2-j}C_B^{B_2+j}j\) 可以變成
組合數收縮公式:
再用範德蒙德卷積:
於是可以單次 \(O(1)\)。 第二部分要求 \(C_A^{A_2-j}C_B^{B_2+j}j[0\leq j]\)。用相同的方法變成:
兩部分是相同的,只考慮其中一部分,那就是要求:
由於每次 \(A+B,A_2+B_2\)為定值,設 \(s_1=A+B,s_2=A_2+B_2\)。考慮其組合意義,相當於有 \(S_1\) 個球,其中要放 \(S_2\) 個黑球,並且要求前 \(A\) 個球中只有 \(\leq A_2\) 個黑球的方案數。
考慮在這條鏈上,從 \(i\rightarrow i+1\) 的時候,\(A\) 和 \(A_2\) 都只會有 \(O(1)\) 的變化。如果能 \(O(1)\) 移動 \(A\pm 1,A_2\pm 1\) 並且實時維護答案,那就能做了。 如果 \(A_2\rightarrow A_2+1\) ,答案只需要加上新的貢獻 \(C_A^{A_2+1}C_{S_1-A}^{S_2-A_2-1}\)。如果 \(A\rightarrow A+1\),答案會減少,再考慮其組合意義,減少的量就是 “第 \(A_2+1\) 個黑球放在 \(A+1\) 位置”的方案數。於是答案只需要減去 \(C_A^{A_2}C_{S_1-A-1}^{S_2-A_2-1}\)。於是我們解決了 \(O(1)\) 移動端點的問題,整個問題可以 \(O(n)\) 解決。
#include<bits/stdc++.h>
#define For(i,l,r) for(int i=(l);i<=(r);++i)
#define ReFor(i,r,l) for(int i=(r);i>=(l);--i)
const int N=1000010;
const int mod=1000000007;
typedef long long ll;
using namespace std;
int n;
int S[N],T[N],S_pre_cnt[3],S_suf_cnt[3],T_pre_cnt[3],T_suf_cnt[3];
ll fac[N],invfac[N];
template<typename T1,typename T2>
void Add(T1 &a,T2 b){a+=b;if(a>=mod)a-=mod;return;}
template<typename T1,typename T2>
void Sub(T1 &a,T2 b){a-=b;if(a<0)a+=mod;return;}
ll qpow(ll a,int b)
{
ll res=1;
while(b)
{
if(b&1)(res*=a)%=mod;
(a*=a)%=mod;b>>=1;
}
return res;
}
ll binom(int n,int m)
{
if((n<0) || (m<0) || (n<m))return 0;
ll res=fac[n];(res*=invfac[m])%=mod;(res*=invfac[n-m])%=mod;return res;
}
struct Binom
{
int lim,A,S,S1;
ll res;
void clear(){res=0;lim=0;A=0;S=0;S1=0;return;}
void brute_calc(){res=0;For(j,0,lim){ll delta=binom(A,j);(delta*=binom((S-A),(S1-j)))%=mod;Add(res,delta);}return;}
void add_lim(){++lim;ll delta=binom(A,lim);(delta*=binom((S-A),(S1-lim)))%=mod;Add(res,delta);return;}
void sub_lim(){ll delta=binom(A,lim);(delta*=binom((S-A),(S1-lim)))%=mod;Sub(res,delta);--lim;return;}
void add_A(){ll delta=binom(A,lim);(delta*=binom((S-A-1),(S1-lim-1)))%=mod;Sub(res,delta);++A;return;}
}Part_1,Part_2;
void solve()
{
cin>>n;For(i,1,n){char ch;cin>>ch;if(ch!='?')S[i]=(ch-'0');else S[i]=2;}For(i,1,n){char ch;cin>>ch;if(ch!='?')T[i]=(ch-'0');else T[i]=2;}
For(i,1,n){if(!(i&1)){if(S[i]!=2)S[i]^=1;if(T[i]!=2)T[i]^=1;}}
ll ans=0;For(i,0,2){S_pre_cnt[i]=0;T_pre_cnt[i]=0;S_suf_cnt[i]=0;T_suf_cnt[i]=0;}For(i,1,n){++S_suf_cnt[S[i]];++T_suf_cnt[T[i]];}
Part_1.clear();Part_2.clear();
Part_1.S=(S_suf_cnt[2]+T_suf_cnt[2]-1);Part_1.S1=(T_suf_cnt[2]-(S_suf_cnt[0]-T_suf_cnt[0])-1);
Part_2.S=(S_suf_cnt[2]+T_suf_cnt[2]);Part_2.S1=(T_suf_cnt[2]-(S_suf_cnt[0]-T_suf_cnt[0]));
For(i,1,n-1)
{
++S_pre_cnt[S[i]];--S_suf_cnt[S[i]];++T_pre_cnt[T[i]];--T_suf_cnt[T[i]];
int A=(S_pre_cnt[2]+T_pre_cnt[2]),A1=(T_pre_cnt[2]+(T_pre_cnt[0]-S_pre_cnt[0]));
int B=(S_suf_cnt[2]+T_suf_cnt[2]),B1=(T_suf_cnt[2]-(S_suf_cnt[0]-T_suf_cnt[0]));
if(i==1){Part_1.lim=(A1-1);Part_1.A=A;Part_1.brute_calc();Part_2.lim=(A1-1);Part_2.A=A;Part_2.brute_calc();}
else
{
if(S[i]==0){Part_1.sub_lim();Part_2.sub_lim();}
if(S[i]==2){Part_1.add_A();Part_2.add_A();}
if(T[i]==0){Part_1.add_lim();Part_2.add_lim();}
if(T[i]==2){Part_1.add_lim();Part_1.add_A();Part_2.add_lim();Part_2.add_A();}
}
{
ll res1=B;if(res1<0)Add(res1,mod);(res1*=binom((A+B-1),(A1+B1-1)))%=mod;ll res2=B1;if(res2<0)Add(res2,mod);(res2*=binom((A+B),(A1+B1)))%=mod;
ll res=res1;Sub(res,res2);Sub(ans,res);
};
{ll res=Part_1.res;(res*=B)%=mod;if(res<0)Add(res,mod);(res*=2)%=mod;Add(ans,res);};
{ll res=Part_2.res;(res*=B1)%=mod;if(res<0)Add(res,mod);(res*=2)%=mod;Sub(ans,res);};
}
cout<<ans<<"\n";return;
}
int main()
{
fac[0]=1;For(i,1,N-1){fac[i]=i;(fac[i]*=fac[i-1])%=mod;}
invfac[N-1]=qpow(fac[N-1],(mod-2));ReFor(i,N-2,1){invfac[i]=(i+1);(invfac[i]*=invfac[i+1])%=mod;}invfac[0]=1;
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t;cin>>t;while(t--)solve();return 0;
}
D 傳火
最佳方案就是所有人都朝 \(k\) 號跑,遇到了就跟隨,\(k\) 號火炬要燃盡時就點燃一根續上,所以每個跟上的人相當於將燃燒時間 \(+t\) 秒。考慮二分答案,轉化成以下問題:
有兩個佇列,要按順序點燃,每個物品點燃要代價 \(a_i\),會獲得 \(t\) 的收益,初始收益為 \(t\),要求任何時候收益不能為負,問是否能全部點燃。
考慮將兩個佇列分成若干組,每組總收益為正,但任意一個字首收益為負。
如果可以全部分組,那麼可以貪心選擇,能點盡點,如果某時刻不行答案就為否,否則回答是。
如果不能全部分組,那麼將不能分組的物品拿出來,它們點燃每個字首收益會減少。
注意到最後總收益是確定的,因此可以時間倒流,將貢獻加上 \(\sum a_i-t\),並將這些物品翻轉,就成了順序點燃,每點燃一個物品付出 \(t\) 的代價,獲得 \(a_i\) 收益,要求任何時候收益不為負。
因此可以將貢獻取負,倒著搞一下之前的演算法即可。
#include <bits/stdc++.h>
using namespace std;
template <typename T>
inline void read(T &x) {
x = 0;
short f = 1;
char c = getchar();
for (; c < '0' || c > '9'; c = getchar())
if (c == '-')
f = -1;
for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ 48);
x *= f;
return;
}
#define ll long long
const int N = 1e6 + 5;
ll x[N], a[N];
int n, k, t;
bool check(ll v) {
for (int i = 1; i <= n; ++i) a[i] = x[i] - 2ll * v * i * t;
if (a[1] < a[n])
return 0;
int L = k, R = k;
for (int i = 1; i <= k - 1; ++i)
if (a[i] > a[L])
L = i;
for (int i = k + 1; i <= n; ++i)
if (a[i] <= a[R])
R = i;
int l = k, r = k;
while (l != L || r != R) {
bool flag = 0;
int tl = l, tr = r;
while (tl > L && a[tl - 1] >= a[r])
if (a[--tl] >= a[l])
break;
if (tl < l && a[tl] >= a[l])
flag = 1, l = tl;
while (tr < R && a[tr + 1] <= a[l])
if (a[++tr] <= a[r])
break;
if (tr > r && a[tr] <= a[r])
flag = 1, r = tr;
if (!flag)
return 0;
}
l = 1, r = n;
while (l != L || r != R) {
bool flag = 0;
int tl = l, tr = r;
while (tl < L && a[tl + 1] >= a[r])
if (a[++tl] >= a[l])
break;
if (tl > l && a[tl] >= a[l])
flag = 1, l = tl;
while (tr > R && a[tr - 1] <= a[l])
if (a[--tr] <= a[r])
break;
if (tr < r && a[tr] <= a[r])
flag = 1, r = tr;
if (!flag)
return 0;
}
return 1;
}
int main() {
read(n), read(k), read(t);
for (int i = 1; i <= n; ++i) read(x[i]);
ll l = 0, r = 1e9, ans = r;
while (l <= r) {
ll mid = (l + r) >> 1;
if (check(mid))
r = mid - 1, ans = mid;
else
l = mid + 1;
}
printf("%lld\n", ans);
return 0;
}
E 樹權
容易發現答案一定包含直徑的某個端點,列舉是哪個端點,以這個端點為根建樹,進行長鏈剖分。
此時答案一定選擇了根到一個葉子結點的路徑,以及 \(k-1\) 對葉子,可以看成是選擇了 \(2k-1\) 個葉子。
貪心地進行選擇,一個葉子的貢獻是它到它所在鏈頂點的距離,只需選出貢獻最大的 \(2k-1\) 個葉子,容易證明一定存在使得這種貪心合法的方案。
接下來考慮必須覆蓋點 \(a\) 的限制,將葉子按貢獻從大到小排序,令 \(h_x\) 為 \(x\) 子樹內葉子排名的最小值。對於點 \(x\),若 \(h_x\leq 2k-1\),則直接貪心的方案合法。否則要把一個葉子換成 \(x\) 子樹內的葉子。假設換掉了葉子 \(y\):
若 \(y\) 的頂端不是 \(x\) 的祖先,那我們換掉它的代價就是 \(y\) 的貢獻。
否則代價就是 \(y\) 到 \(LCA(x,y)\) 的距離。
第一種情況可以直接換掉貢獻最小的葉子,第二種情況可以倍增解決。
#include <bits/stdc++.h>
using namespace std;
char buf[1000000], *p1 = buf, *p2 = buf;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1000000, stdin), p1 == p2) ? EOF : *p1++)
template <typename T>
inline void read(T &x) {
x = 0;
short f = 1;
char c = getchar();
for (; c < '0' || c > '9'; c = getchar())
if (c == '-')
f = -1;
for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ 48);
x *= f;
return;
}
char out[1 << 23], *pout = out, *eout = out + (1 << 23);
inline void flush() { fwrite(out, 1, pout - out, stdout), pout = out; }
inline void pc(char c) {
pout == eout && (fwrite(out, 1, 1 << 23, stdout), pout = out);
(*pout++) = c;
}
template <typename T>
inline void print(T x) {
static int stk[35], top;
if (x < 0)
pc('-'), x = -x;
if (x == 0)
pc('0');
while (x) stk[++top] = x % 10, x /= 10;
while (top) pc(stk[top--] + '0');
pc('\n');
}
const int N = 5e5 + 5;
#define ll long long
struct node {
int v, w, nxt;
} e[N * 2];
int head[N], cnt = 1;
void add(int u, int v, int w) {
e[cnt].v = v;
e[cnt].w = w;
e[cnt].nxt = head[u];
head[u] = cnt++;
}
struct Tree {
int rt;
int son[N], f[N][20], rk[N];
int p[N], tot;
ll ans[N], len[N], dis[N], maxd[N];
void dfs1(int u, int fa) {
if (dis[u] > dis[rt])
rt = u;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (v == fa)
continue;
dis[v] = dis[u] + e[i].w;
dfs1(v, u);
}
}
void dfs2(int u, int fa) {
f[u][0] = fa;
for (int k = 1; k <= 19; ++k) f[u][k] = f[f[u][k - 1]][k - 1];
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (v == fa)
continue;
dis[v] = dis[u] + w;
dfs2(v, u);
if (w + maxd[v] > maxd[u])
maxd[u] = maxd[v] + w, son[u] = v;
}
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (v == fa || v == son[u])
continue;
p[++tot] = v;
len[v] = maxd[v] + e[i].w;
}
if (!fa)
p[++tot] = u, len[u] = maxd[u];
}
void init(int x) {
dfs1(x, 0);
dis[rt] = 0;
dfs2(rt, 0);
sort(p + 1, p + tot + 1, [&](int x, int y) { return len[x] > len[y]; });
for (int i = 1; i <= tot; ++i) {
ans[i] = ans[i - 1] + len[p[i]];
for (int u = p[i]; u; u = son[u]) rk[u] = i;
}
}
ll ask(int x, int y, int tp) {
ll w = maxd[x] + dis[x] + ans[y];
for (int k = 19; k >= 0; --k)
if (rk[f[x][k]] > y)
x = f[x][k];
return w - dis[f[x][0]] - (tp ? maxd[f[x][0]] : 0);
}
ll query(int x, int y) {
y = 2 * y - 1;
return rk[x] > y ? max(ask(x, y - 1, 0), ask(x, y, 1)) : ans[min(tot, y)];
}
} A, B;
int n, m;
int main() {
read(n);
read(m);
for (int i = 1; i < n; ++i) {
int u, v, w;
read(u), read(v), read(w);
add(u, v, w), add(v, u, w);
}
A.init(1);
B.init(A.rt);
while (m--) {
int a, k;
read(a), read(k);
print(max(A.query(a, k), B.query(a, k)));
}
flush();
return 0;
}