MX 暑假集训 7.24
Mili - Peach Pit and Cyanide(桃核与氰化物)【OWC】
哈哈哈是我喜欢的图论。
[THUPC 2023 初赛] 背包
题意
有 \(n\) 种物品,第 \(i\) 种体积为 \(v_i\),价值为 \(c_i\),每种物品可以选任意多次,\(q\) 次询问,每次询问给出 \(V\),求总体积恰为 \(V\) 的物品的最大价值,无解输出 \(-1\)。
\(1\le n\le 50,1\le v_i\le 10^5,1\le c_i\le 10^6,1\le q\le 10^5,10^{11}\le V\le 10^{12}\)。
solution
同余最短路,本质上是对于每个余数记录信息,这题记录的是最大价值。
由于 \(V\) 很大,贪心地想,我们需要放入很多性价比最大的物品,那么令性价比最大的物品体积为 \(m\),价值为 \(w\)。
对一个体积为 \(v\),价值为 \(c\) 的方案,我们把性价比最大的物品所能提供的线性价值减去,令 \(\Delta = c-\left\lfloor \frac{v}{m}\right\rfloor w\),那么我们只需要对于每个余数 \(r\),最大化 $\Delta $ 的值,将这个值加上 \(\left\lfloor \frac{V}{m}\right\rfloor w\) 即为体积为 \(V\) 的答案,由于 \(V\) 很大,可以保证每个调整的方案所用体积 \(v\) 不会超过 \(V\) 而导致方案失效。
令 \(f_r\) 为余数为 \(r\) 时的最大 $\Delta $,那么对于每个物品 \((v,c)\),有转移 \(f_{(r+v)\mod m}\leftarrow \max(f_{(r+v)\mod m},f_r+c-\left\lfloor \frac{r+v}{m}\right\rfloor w)\),一个经典的思路就是在同余系下直接建图跑最长路。
SPFA 的效率是很低的,我们考虑这个图有没有更优秀的性质。
对于一个物品 \((v,c)\),我们发现它的转移路径可以分成 \(\gcd (v,m)\) 个不交的环,那么我们可以对每个环转移两圈,因为不存在正环,所以第三圈及以后的转移不会更新更优的值,所以做两圈的转移即可将最优状态全部转移,每个物品转移的时间复杂度是 \(O(m)\) 的。
时间复杂度 \(O(nm+q)\),其中 \(m\) 为性价比最大物品的体积。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr ll inf=1e18;
inline int gcd(int x,int y) {return y?gcd(y,x%y):x;}
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
inline void readl(ll &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n,q,w,m=1;
int v[N],c[N];
ll dis[N];
int main() {
read(n);read(q);
f(i,1,n) {
read(v[i]);read(c[i]);
ll v1=1ll*c[i]*m,v2=1ll*v[i]*w;
if (v1>v2||(v1==v2&&v[i]<m)) w=c[i],m=v[i];
}
f(i,1,m) dis[i]=-inf;
f(i,1,n) {
int md=v[i]%m,d=v[i]/m,val=c[i];
for (int j=0,g=gcd(v[i],m);j<g;j++)
for (int k=j,t=0;t<2;t+=(k==j)) {
int nxt=k+md,cnt=d;
if (nxt>=m) nxt-=m,cnt++;
dis[nxt]=max(dis[nxt],dis[k]+val-1ll*cnt*w);
k=nxt;
}
}
while (q--) {
ll V;readl(V);int p=V%m;
if (dis[p]<-1e16) puts("-1");
else printf("%lld\n",dis[p]+(V/m)*w);
}
return 0;
}
Max Vector
题意
给定两个长度为 \(n\) 的序列 \(X,Y\),你需要进行 \(m\) 次操作,每次操作给定一个长度为 \(n\) 的数组 \(A\),你需要选择以下两种方式的一种进行操作。
- 对于 \(1\le i\le n\),\(X_i\leftarrow \max{(X_i,A_i)}\);
- 对于 \(1\le i\le n\),\(Y_i\leftarrow \max{(Y_i,A_i)}\)。
你需要最小化 \(m\) 次操作后 \(\sum\limits_{i=1}^{n} (X_i+Y_i)\) 的值,输出最小值。
\(1\le n\le 10,1\le m\le 500,1\le X_i,Y_i,A_i\le 500\)。
solution
很经典的建模思路,但是我第一次见。
二选一,考虑转化成最小割。
我们可以连出 \(n\) 条长度为 \(V\) 的链(\(V\) 为值域),第 \(i\) 条链表示 \(X_i\) 最终的取值,每条链上第 \(j\) 条边的容量为 \(j\),在这条链上割掉一条边,代表了 \(X_i\) 就为这条边的容量,对 \(Y_i\) 也同理连出 \(n\) 条链。
我们逐步考虑如何处理题目的限制。
先考虑每个操作,不妨先考虑 \(n=1\),那么对于一个操作给定的 \(A\),要么有 \(X_1\ge A_1\),要么有 \(Y_1\ge A_1\),我们可以将 \(Y\) 对应的链倒过来,即 \(S\) 向 \(T\) 在 \(Y\) 这条链上的权值从 \(V\) 到 \(1\) 单调递减,而 \(S\) 向 \(T\) 在 \(X\) 这条链上的权值从 \(1\) 到 \(V\) 单调递增,那么一个限制就可加一条边,从 \(Y\) 的 \(A_1\) 权值位置向 \(X\) 的 \(A_1\) 权值位置连容量极大的边,那么我们发现,如果 \(Y_1\) 取了比 \(A_1\) 小的值,那么 \(X_1\) 就不能选择比 \(A_1\) 小的值了,因为这样就存在一条 \(S\rightarrow A_1(Y)\rightarrow A_1(X)\rightarrow T\) 的增广路,所以必须在 \(Y\) 大于 \(A_1\) 的位置割掉一条边或在 \(X\) 大于 \(A_1\) 的位置割掉一条边。
那么 \(n>1\) 怎么做呢?类似地,我们对每个操作建虚点,\(Y_i\) 的链的 \(A_i\) 位置向虚点连容量为极大值的边,虚点向 \(X_i\) 的 \(A_i\) 位置连容量为极大值的边,这样就限制了所有的 \(X_i\) 或所有的 \(Y_i\) 都必须满足大于 \(A_i\),否则也能找到一条增广路。
操作的限制处理完了,考虑一下初始的限制怎么处理。
比较简单,在 \(X_i\) 的链上的连从 \(S\) 向初始值位置连容量为极大值的边,在 \(Y_i\) 的链上连从初始值位置向 \(T\) 连容量为极大值的边即可,本质上也是一个不小于一个数的限制。
还要防止一条链上割掉两条边,只需要链上的每条边从后往前连一条容量为极大值的边即可,这道题似乎不会出现同一条链割两条边的情况,但是其他题用这样的建模思路加上链上的反向边就可以防止一条链割两条边的情况。
于是做完了,点数 \(O(nV+m)\),边数 \(O(n(m+V))\)。
代码要注意一些细节,需要对于边的位置和方向具体分析连接左边节点还是右边节点,但核心思路就是限制不能割掉一些位置的边。
Code
#include<cstdio>
#include<algorithm>
#include<queue>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e3+10;
constexpr int V=500;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int s,t,cnt;
namespace Flow {
constexpr int N=2e5+10;
constexpr int M=2e6+10;
int head[N],tot=1;
struct Edge{int next,to,w;}e[M];
inline void add(int u,int v,int w) {
e[++tot]={head[u],v,w};
head[u]=tot;
e[++tot]={head[v],u,0};
head[v]=tot;
}
int dis[N],del[N];
inline bool spfa() {
f(i,1,cnt) dis[i]=inf;
dis[s]=0;
del[s]=head[s];
queue<int> q;
q.push(s);
while (!q.empty()) {
int now=q.front();
q.pop();
for (int i=head[now];i;i=e[i].next) {
int v=e[i].to,w=e[i].w;
if (dis[v]!=inf||!w) continue;
dis[v]=dis[now]+1;
del[v]=head[v];
q.push(v);
if (v==t) return 1;
}
}
return 0;
}
inline int dfs(int now,int sum) {
if (now==t) return sum;
int res=0;
for (int i=del[now];i&∑i=e[i].next) {
del[now]=i;
int v=e[i].to,w=e[i].w;
if (dis[v]!=dis[now]+1||!w) continue;
int k=dfs(v,min(sum,w));
if (!k) dis[v]=inf;
e[i].w-=k;e[i^1].w+=k;
res+=k;sum-=k;
}
return res;
}
inline int dinic() {
int res=0;
while (spfa()) res+=dfs(s,inf);
return res;
}
}
int n,m;
int id1[15][N],id2[15][N];
using Flow::add;
int main() {
read(n);read(m);s=++cnt;t=++cnt;
f(i,1,n) f(j,1,V) {
id1[i][j]=++cnt;
id2[i][j]=++cnt;
}
f(i,1,n) {
add(s,id1[i][1],1);
add(id1[i][V],t,inf);
add(s,id2[i][V],V);
add(id2[i][1],t,inf);
f(j,2,V) {
add(id1[i][j-1],id1[i][j],j);
add(id1[i][j],id1[i][j-1],inf);
add(id2[i][j],id2[i][j-1],j-1);
add(id2[i][j-1],id2[i][j],inf);
}
}
f(i,1,n) {
int x;read(x);
if (x!=1) add(s,id1[i][x-1],inf);
}
f(i,1,n) {
int x;read(x);
add(id2[i][x],t,inf);
}
f(i,1,m) {
cnt++;
f(j,1,n) {
int x;read(x);
add(id2[j][x],cnt,inf);
if (x!=1) add(cnt,id1[j][x-1],inf);
else add(cnt,s,inf);
}
}
printf("%d\n",Flow::dinic());
return 0;
}
[湖北省选模拟 2023] 棋圣 / alphago
题意
给定一张 \(n\) 个点 \(m\) 条边的无向连通图,边有边权,在其中 \(k\) 个点上有黑棋或白棋中的一种,你可以进行若干次操作,每次操作形式如下:
- 选定一个上面无棋子的点 \(v\)(保证存在这样的点);
- 对于所有棋子,令其在点 \(u\),你需要选择一条 \(u\rightarrow v\) 的简单路径,并将这枚棋子沿着这条路径移动到下一个节点,允许多个棋子在同一节点上。
操作后,定义当前棋子状态的收益为:对于所有颜色不同的棋子对,令其分别在 \(u,v\),若存在一条边 \((u,v,w)\),那么获得 \(w\) 的收益,否则不获得收益,总收益为每对棋子收益之和,求最大收益。
\(1\le k<n\le 100,1\le m\le \frac{n(n-1)}{2}\)。
solution
挺有意思的题。
贪心地想,我们可以找到边权最大的边,让黑白两种棋子分别在这条边的两侧,这样收益一定最多。
首先如果图存在奇环,那么我们可以直接将所有棋子聚集到这个奇环上,找到奇环上一个空点一直进行操作就能做到(如果初始时环上没有空点就对环外的点执行一次操作,使环上空余出一个点),聚集到环上后,发现控制一枚棋子绕环一圈,从而改变棋子在环上的奇偶性,也就能将所有黑棋聚集到一个点,白棋聚集到一个点,然后直接将这些棋子移动到最大权的边两侧即可,也就是说,若存在奇环则收益一定能达到最大值(最大值指的是将黑白棋分别置于最大边权的边的两侧的收益)。
那么我们只需考虑二分图的情况,将二分图黑白染色,为了与棋子颜色区分我们称染为黑色或白色为一个点的奇偶性。
如果是一个偶环那么类似于奇环,但是我们不能改变每个点的奇偶性,所以奇偶性不变同时将黑白棋子都移动到最大边权两侧即可,令 \(c_{i,j}\) 为颜色为 \(i\),奇偶性为 \(j\) 的棋子个数,令权值最大的边边权为 \(mxw\),那么答案显然为 \(mxw\times (c_{0,1}\times c_{1,0}+c_{1,1}\times c_{0,0})\)。
如果原图是一棵树,且叶子节点个数不小于 \(3\),那么我们可以找到 \(3\) 个叶子节点,轮流操作它们,对于每对棋子,一定能找到一个叶子节点使它们的距离缩小,而每次操作不可能使它们距离增大,所以最终所有棋子都会被缩到一条边的两侧,那么我们就可以移动它们到最大边权两侧,同样不能改变奇偶性,所以答案仍为 \(mxw\times (c_{0,1}\times c_{1,0}+c_{1,1}\times c_{0,0})\)。
如果是一个二分图,那么我们可以找出一棵满足上面条件的树并在上面做同样的事,那么答案与上面相同。
不满足上面所有情况的图只可能是一条链,我们发现似乎并不能很快直接找到答案,而 \(n\le 100\),所以考虑 dp。
将这条链从左到右对棋子依次编号,经过观察,若最终的图满足以下性质,则一定可以通过原图得到:
- 相邻两枚棋子距离不增(若操作点在中间则距离减 \(2\),否则不变);
- 从左到右按顺序取出棋子,与原图上取出棋子的顺序一致(证明同上);
- 相邻两枚棋子距离奇偶性不变(每次操作要么减少 \(2\) 要么不变)。
令 \(f_{i,l,r}\) 表示考虑到第 \(i\) 个点,已经放置完 \([1,r]\) 的所有棋子,其中编号在 \([l,r]\) 的棋子都在 \(i\) 上的最大权值。
令 \(cnt_{l_1,r_1,l_2,r_2}\) 表示将 \([l_1,r_1]\) 的棋子与 \([l_2,r_2]\) 的棋子相邻放置,能产生多少对颜色不同的棋子,\(w_{i}\) 表示 \((i,i+1)\) 的边权,\(dis_{i,j}\) 表示原图第 \(i\) 枚棋子到第 \(j\) 枚棋子的距离,考虑前面一个点是否放置棋子,那么 \(f_{i,l,r}\) 有以下转移。
注意 \([l,r]\) 需满足奇偶性相同,初始化 \(f_{i,1,j}=0\),其中 \(1\le i\le n\),\(j\) 需要满足 \([1,j]\) 棋子奇偶性相同。
处理出 \(g_{i,r}=\max\limits_{l=1}^{r}f_{i,l,r}\),即可做到 \(O(n)\) 转移。
时间复杂度 \(O(n^4)\)。
Code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=100+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n,m,k,flag,mxw;
int c[N],col[N],c0,c1;
struct Edge{int v,w;};
vector<Edge> e[N];
inline void dfs(int now,int co) {
col[now]=co;
for (Edge E:e[now]) {
int v=E.v;
if (col[v]==-1) dfs(v,co^1);
else if (col[v]==co) flag=1;
}
}
int st[N],val[N],top;
inline void link_all(int now,int fa) {
st[++top]=now;
for (Edge E:e[now]) if (E.v^fa) {
val[top]=E.w;
link_all(E.v,now);
}
}
int p[N],s0[N],s1[N],p0[N],p1[N];
inline int get_id() {
int s=0,num=0;
f(i,1,n) if (e[i].size()==1) s=i;
link_all(s,0);
static int mp[N];
f(i,1,n) mp[i]=c[st[i]];
f(i,1,n) if (mp[i]) {
p[++num]=i;
c[num]=mp[i]-1;
s0[num]=s0[num-1]+(!c[num]);
s1[num]=s1[num-1]+c[num];
p0[num]=p0[num-1]+((i&1)^1);
p1[num]=p1[num-1]+(i&1);
}
return num;
}
inline int W(int l1,int r1,int l2,int r2,int i) {
int c0_x=s0[r1]-s0[l1-1];
int c1_x=s1[r1]-s1[l1-1];
int c0_y=s0[r2]-s0[l2-1];
int c1_y=s1[r2]-s1[l2-1];
return val[i-1]*(c0_x*c1_y+c0_y*c1_x);
}
inline int dis(int i,int j) {
if (!j) return inf;
return p[i]-p[j];
}
inline bool check(int l,int r) {
int c0=p0[r]-p0[l-1];
int c1=p1[r]-p1[l-1];
if (c0&&c1) return 0;
return 1;
}
inline int solve(int m) {
static int f[N][N][N],g[N][N];
f(i,0,n) f(j,0,m) {
f(k,0,m) f[i][j][k]=-inf;
g[i][j]=-inf;
}
int mx_ok=1;
f(i,1,m) {
if (dis(1,i)&1) break;
else mx_ok=i;
}
f(i,1,n) f(j,1,mx_ok) f[i][1][j]=0;
f(i,1,n) f(l,1,m) f(r,l,m) {
if (!check(l,r)) continue;
int d=dis(l,l-1);
if ((d&1)&&i!=1) {f(j,1,l-1) f[i][l][r]=max(f[i][l][r],f[i-1][j][l-1]+W(j,l-1,l,r,i));}
for (int j=max(1,i-d)+(((i-max(1,i-d))&1)!=(d&1));j<i;j+=2) f[i][l][r]=max(f[i][l][r],g[j][l-1]);
g[i][r]=max(g[i][r],f[i][l][r]);
}
int ret=-inf;
f(i,1,n) f(j,1,m) ret=max(ret,f[i][j][m]);
return ret;
}
int main() {
read(n);read(m);read(k);
f(i,1,k) {
int x,o;read(x);read(o);
c[x]=o+1;c1+=o;c0+=(!o);
}
f(i,1,m) {
int u,v,w;read(u);read(v);
read(w);mxw=max(mxw,w);
e[u].push_back({v,w});
e[v].push_back({u,w});
}
f(i,1,n) col[i]=-1;
dfs(1,0);
if (flag) return printf("%d\n",mxw*c0*c1),0;
f(i,1,n) if (e[i].size()>2) flag=1;
flag|=(m!=n-1);
if (flag) {
static int cnt[2][2];
f(i,1,n) if (c[i]) cnt[c[i]-1][col[i]]++;
return printf("%d\n",mxw*(cnt[0][1]*cnt[1][0]+cnt[1][1]*cnt[0][0])),0;
}
printf("%d\n",solve(get_id()));
return 0;
}
习题
同余最短路:
网络流:
Everywhere is Sparser than Whole (Judge)

浙公网安备 33010602011771号