ABC338 F

link

观察到 n 的范围不大,考虑状压。

dpi,S 表示在经过顶点集合状态为 S 的情况下,终点为 i 的最小步数。

错误示范:

考虑对于状态 S ,直接遍历经过的顶点 i ,枚举 i 的出边进行转移。

#include<bits/stdc++.h>
typedef long long LL;
typedef std::pair<int,int> pii;
#define fi first
#define se second
#define ok putstrln("OrzFinderHT")
//#define check_time printf("%.8f\n",clocks()/CLOCK_PER_SEC)
template<typename T>
void abs(T &N){
if(N>=0) N=N;
else N=-N;
}
namespace G_{
template<typename T>
inline void read(T &a){
a=0;
int f=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') a=a*10+(int)(c-'0'),c=getchar();
a*=f;
}
inline void get_enter(){
getchar();
}
inline void get_str(std::string &str){
char c=getchar();
while(c!=' '&&c!='\n') str+=c,c=getchar();
}
template<typename T>
inline void putN(T N){
char stk[70];
short top=0;
if(N<0) putchar('-'),abs(N);
do{
stk[++top]=(char)(N%10+(int)'0');
N/=10;
}while(N);
while(top) putchar(stk[top--]);
}
template<typename T>
inline void putsp(T N){
putN(N);
putchar(' ');
}
template<typename T>
inline void putln(T N){
putN(N);
putchar('\n');
}
inline void putstr(std::string str){
int sz=str.size()-1;
for(int i=0;i<=sz;i++) putchar(str[i]);
}
inline void putstrln(std::string str){
putstr(str);
putchar('\n');
}
inline void Yes(){
putstrln("Yes");
}
inline void No(){
putstrln("No");
}
template<typename T,typename I>
inline void chkmin(T &a,I b){
a=std::min(a,b);
}
}
//using namespace get_give;
using namespace G_;
const LL maxn=25,maxs=1<<20,inf=1e9;
std::vector<pii>d[maxn];
LL dp[maxn][maxs];
signed main(){
int n,m;
read(n),read(m);
for(int i=1;i<=m;i++) {
int u,v,w;
read(u),read(v),read(w);
d[u].push_back({v,w});
}
int S=(1<<n)-1;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++) dp[i][(1<<(i-1))]=0;
for(int s=1;s<S;s++)
for(int i=1;i<=n;i++){
if(!((1<<(i-1))&s)) continue;
for(auto q:d[i]){
// if((1<<(q.fi-1))&s) continue;
int g=s|(1<<(q.fi-1));
chkmin(dp[q.fi][g],dp[i][s]+q.se);
// printf("##%d %d %d %d !!%d\n",q.fi,g,i,s,dp[q.fi][g]);
}
}
LL ans=inf;
for(int i=1;i<=n;i++) chkmin(ans,dp[i][S]);//,printf("&&&%d %d\n",i,ans);
if(ans==inf){
printf("No");
return 0;
}
printf("%lld\n",ans);
// putN(ans);
return 0;
}
/*
-读入字符一定检查回车
- 能不能搜索?
-函数要有返回值!
-想好了再写!
*/

由于存在值为负的路径,所以使得我们重复经过一些边可以让答案更优。比如对于 11010 ,我们的转移一定来自 11000 或 10010 或 01010 。但是注意,当我们在 11010 这个状态到达 #2 时,若连接 #2 和 #4 的边是负的,那么我们可以通过这条边,使得 11010 状态下代价变小。

那如果我们允许从一个访问过的点到达另一个访问过的点呢?

还是对于 11010 ,我们会按照 245 的顺序来访问已知点,令 w(i,j) 表示连接 ij 两点的边的权值,如果 w(5,2)+w(2,4)<0 ,那么我们又会得到一个更小的 11010 状态下的代价,但是遗憾的是,当我们由 5 更新到 2 时,无法再退回去进一步更新。

我们又想到,可以用队列记录可能更新其它状态的状态,类似 dij 那样,但是如果用队列处理,会导致时间复杂度过大。

但是发现,只会有三个点 TLE ,且时间只比时限多出 0.6s(link),运用一些随机化技巧或许可以通过,可以自行尝试。

#include<bits/stdc++.h>
typedef long long LL;
typedef std::pair<int,int> pii;
#define fi first
#define se second
#define ok putstrln("OrzFinderHT")
//#define check_time printf("%.8f\n",clocks()/CLOCK_PER_SEC)
template<typename T>
void abs(T &N){
if(N>=0) N=N;
else N=-N;
}
namespace G_{
template<typename T>
inline void read(T &a){
a=0;
int f=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') a=a*10+(int)(c-'0'),c=getchar();
a*=f;
}
inline void get_enter(){
getchar();
}
inline void get_str(std::string &str){
char c=getchar();
while(c!=' '&&c!='\n') str+=c,c=getchar();
}
template<typename T>
inline void putN(T N){
char stk[70];
short top=0;
if(N<0) putchar('-'),abs(N);
do{
stk[++top]=(char)(N%10+(int)'0');
N/=10;
}while(N);
while(top) putchar(stk[top--]);
}
template<typename T>
inline void putsp(T N){
putN(N);
putchar(' ');
}
template<typename T>
inline void putln(T N){
putN(N);
putchar('\n');
}
inline void putstr(std::string str){
int sz=str.size()-1;
for(int i=0;i<=sz;i++) putchar(str[i]);
}
inline void putstrln(std::string str){
putstr(str);
putchar('\n');
}
inline void Yes(){
putstrln("Yes");
}
inline void No(){
putstrln("No");
}
template<typename T,typename I>
inline void chkmin(T &a,I b){
a=std::min(a,b);
}
}
//using namespace get_give;
using namespace G_;
const LL maxn=25,maxs=1<<20,inf=1e9;
std::vector<pii>d[maxn];
LL dp[maxn][maxs];
std::queue<pii>q;
signed main(){
int n,m;
read(n),read(m);
for(int i=1;i<=m;i++) {
int u,v,w;
read(u),read(v),read(w);
d[u].push_back({v,w});
}
int S=(1<<n)-1;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++) dp[i][(1<<(i-1))]=0,q.push({i,1<<(i-1)});
while(q.size()){
int s=q.front().se,i=q.front().fi;
q.pop();
for(int flag=1;flag;flag--){
if(!((1<<(i-1))&s)) continue;
for(auto qq:d[i]){
// if((1<<(q.fi-1))&s) continue;
int g=s|(1<<(qq.fi-1));
if(dp[qq.fi][g]>dp[i][s]+qq.se) q.push({qq.fi,g});
chkmin(dp[qq.fi][g],dp[i][s]+qq.se);
// printf("##%d %d %d %d !!%d\n",q.fi,g,i,s,dp[q.fi][g]);
}
}
}
LL ans=inf;
for(int i=1;i<=n;i++) chkmin(ans,dp[i][S]);//,printf("&&&%d %d\n",i,ans);
if(ans==inf){
printf("No");
return 0;
}
printf("%lld\n",ans);
// putN(ans);
return 0;
}
/*
-读入字符一定检查回车
- 能不能搜索?
-函数要有返回值!
-想好了再写!
*/

参见:link

正确示范:

考虑用 floyd 先处理出来两点之间的最短路,然后不枚举连边,直接对所有点更新,可以避免上面那种情况。

但是,我们注意到如果从 ij 更新,且两者不存在直接连边,那样就会经过一些没有访问过的点,这些点不会被记录进状态中,这样是否会造成答案错误?

显然是不会的,设当前状态为 S ,按照我们的方法,能保证所有 S 的状态是正确的。对于后面的状态,经过感性理解可以发现也是没有影响的。

官方题解中先给出了一段看似显然的证明,或许意在利用 dp 的最优子结构性质来进行 dp ,但说明不够充分,大部分还需要感性理解。(显然,我的题解更适合作为官方题解)。

#include<bits/stdc++.h>
typedef long long LL;
typedef std::pair<int,int> pii;
#define fi first
#define se second
#define ok putstrln("OrzFinderHT")
//#define check_time printf("%.8f\n",clocks()/CLOCK_PER_SEC)
template<typename T>
void abs(T &N){
if(N>=0) N=N;
else N=-N;
}
namespace G_{
template<typename T>
inline void read(T &a){
a=0;
int f=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') a=a*10+(int)(c-'0'),c=getchar();
a*=f;
}
inline void get_enter(){
getchar();
}
inline void get_str(std::string &str){
char c=getchar();
while(c!=' '&&c!='\n') str+=c,c=getchar();
}
template<typename T>
inline void putN(T N){
char stk[70];
short top=0;
if(N<0) putchar('-'),abs(N);
do{
stk[++top]=(char)(N%10+(int)'0');
N/=10;
}while(N);
while(top) putchar(stk[top--]);
}
template<typename T>
inline void putsp(T N){
putN(N);
putchar(' ');
}
template<typename T>
inline void putln(T N){
putN(N);
putchar('\n');
}
inline void putstr(std::string str){
int sz=str.size()-1;
for(int i=0;i<=sz;i++) putchar(str[i]);
}
inline void putstrln(std::string str){
putstr(str);
putchar('\n');
}
inline void Yes(){
putstrln("Yes");
}
inline void No(){
putstrln("No");
}
template<typename T,typename I>
inline void chkmin(T &a,I b){
a=std::min(a,b);
}
}
//using namespace get_give;
using namespace G_;
const LL maxn=25,maxs=1<<20,inf=1e9;
std::vector<pii>d[maxn];
LL dp[maxn][maxs];
int dis[maxn][maxn];
signed main(){
int n,m;
read(n),read(m);
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=m;i++) {
int u,v,w;
read(u),read(v),read(w);
dis[u][v]=w;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++) chkmin(dis[i][j],dis[i][k]+dis[k][j]);
int S=(1<<n)-1;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++) dp[i][(1<<(i-1))]=0;
for(int s=1;s<S;s++)
for(int i=1;i<=n;i++){
if(!((1<<(i-1))&s)) continue;
for(int j=1;j<=n;j++){
// if((1<<(q.fi-1))&s) continue;
int g=s|(1<<(j-1));
chkmin(dp[j][g],dp[i][s]+dis[i][j]);
// printf("##%d %d %d %d !!%d\n",q.fi,g,i,s,dp[q.fi][g]);
}
}
LL ans=inf;
for(int i=1;i<=n;i++) chkmin(ans,dp[i][S]);//,printf("&&&%d %d\n",i,ans);
if(ans==inf){
printf("No");
return 0;
}
printf("%lld\n",ans);
// putN(ans);
return 0;
}
/*
-读入字符一定检查回车
- 能不能搜索?
-函数要有返回值!
-想好了再写!
*/

时间复杂度

参见此贴

贴中 rui_er 指出状压 dp 基础复杂度就是 O(2(n1)×n) ,但至于“常数巨大”,我并没有发现什么常数。

本题的最优复杂度为 O(n3+2n1×n2) (lowbit 逐位分解),但在通常实现方式下为 O(n3+2n×n2)

posted @   BYR_KKK  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示