当前位置: 首页 > news >正文

Petrozavodsk Summer 2024. Day 1. Welcome Contest

Preface

整个国庆就训两天,摆烂这一块

这场感觉前期题里计数偏多,只能说毛子场是这样的;然后有人写 D 的时候猪脑过载,占了 90 min 机时导致队友早就会了的 B 没时间写,我不说是谁


A. Adding Integers

把组合式子拆开发现对原序列求差分 \(d_i=a_i-a_{i-1}\) 后,组合意义为:把 \(n\) 个球放到 \(q+1\) 个盒子里,其中第 \(i\) 个盒子里放 \(d_i\) 个球

对所有的 \(\{d\}\) 形态求和后就是 \(n\) 个带标号球放 \(q+1\) 个带标号盒子,方案数为 \((q+1)^n\),数据范围允许暴力快速幂通过

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int mod=998244353;
inline int quick_pow(int x,int p=mod-2,int mul=1)
{for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
int main()
{int t,n,k;for (scanf("%d",&t);t;--t){scanf("%d%d",&n,&k);int ans=0;for (RI i=1;i<=k;++i)(ans+=quick_pow(i+1,n))%=mod;printf("%d\n",ans);}return 0;
}

B. Bottles

把所有东西都看成有标号的,然后考虑枚举第一次喝解药的位置 \(fir\)

首先把剩下的 \(e-1\) 解药填入后面 \(e+p+w-fir\) 个位置里,然后考虑从发作时间短的毒药开始考虑

\(pos=\max(0,fir-t-1)\),则每个会致死的毒药都恰有 \(e+p+w-e-pos=p+w-pos\) 种选法

对于剩下的毒药和水,放哪里都无所谓,贡献自然很好维护

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=300005,mod=998244353;
int e,p,w,t,fact[N],ifac[N];
inline int quick_pow(int x,int p=mod-2,int mul=1)
{for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{fact[0]=1;for (RI i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;ifac[n]=quick_pow(fact[n]);for (RI i=n-1;i>=0;--i) ifac[i]=1LL*ifac[i+1]*(i+1)%mod;
}
inline int C(CI n,CI m)
{if (n<0||m<0||n<m) return 0;return 1LL*fact[n]*ifac[m]%mod*ifac[n-m]%mod;
}
int main()
{scanf("%d%d%d%d",&e,&p,&w,&t);init(e+p+w); int ans=0;for (RI fir=1;fir<=e+p+w;++fir){int cur=1LL*e*C(e+p+w-fir,e-1)%mod*fact[e-1]%mod;int pos=max(0,fir-t-1);cur=1LL*cur*quick_pow(max(0,p+w-pos),min(pos,p))%mod;cur=1LL*cur*C(p+w-pos,max(0,p-pos))%mod*fact[max(0,p-pos)]%mod;cur=1LL*cur*fact[w]%mod;(ans+=cur)%=mod;}return printf("%d",1LL*ans*ifac[e+p+w]%mod),0;
}

C. Counting Orthogonal Pairs

队友开局推的

#include<bits/stdc++.h>
using namespace std;
#define LL long longLL solve() {int x; cin >> x;if (x % 2 == 1) return 0;else return 1LL* (x/2-1) * x;
}signed main() {ios::sync_with_stdio(0); cin.tie(0);int t; cin >> t; while (t--) cout << solve() << '\n';return 0;
}

D. Divine Tree

随便选一个点当根,考虑最后如果是把所有的 G 扔到子树 \(x\) 里(移动 B 进去的方案同理)

这个代价等价于把 \(x\) 子树里所有 B 移动到点 \(x\) 的距离和,再加上把子树 \(x\) 外部的所有 B 移动到 \(x\) 的距离和

由于修改不改变每个点的信息,因此答案只能在一部分点出取得,可以预处理出这些位置

考虑对一条边 \(x\to y\) 的修改会影响三部分的答案,这个需要画图手动推导一下

最终要实现的就是子树加,树上路径加,以及抛出掉上述两种情况的部分再整体加

写题解的时候想到一个好的实现方法时先全局加上第三种情况的值,然后前两种情况的增量减去第三种情况的增量即可

但当时实现的时候没管那么多了,直接暴力把拆分出的区间(个数是 \(\log\) 级别的)拎出来把中间的补暴力跑一遍修改,复杂度也没啥区别

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#include<assert.h>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=100005,INF=1e18;
int n,a[N],anc[N],u[N],v[N],w[N],key[N],seq[N],q,e[N],d[N],ans[N];
char s[N]; vector <int> E[N];
class Segment_Tree
{private:int mn[N<<2],tag[N<<2];inline void apply(CI now,CI mv){mn[now]+=mv; tag[now]+=mv;}inline void pushup(CI now){mn[now]=min(mn[now<<1],mn[now<<1|1]);}inline void pushdown(CI now){if (tag[now]) apply(now<<1,tag[now]),apply(now<<1|1,tag[now]),tag[now]=0;}public:#define TN CI now=1,CI l=1,CI r=n#define LS now<<1,l,mid#define RS now<<1|1,mid+1,rinline void build(TN){tag[now]=0; if (l==r) return (void)(mn[now]=(key[seq[l]]?0:INF));int mid=l+r>>1; build(LS); build(RS); pushup(now);}inline void modify(CI beg,CI end,CI mv,TN){if (beg<=l&&r<=end) return apply(now,mv);int mid=l+r>>1; pushdown(now);if (beg<=mid) modify(beg,end,mv,LS);if (end>mid) modify(beg,end,mv,RS);pushup(now);}inline int query(void){return mn[1];}#undef TN#undef LS#undef RS
}SEG;
vector <pair <int,int>> itv;
namespace Heavy_Division
{int dep[N],dfn[N],top[N],idx,sz[N],son[N];inline void DFS1(CI now=1,CI fa=0){sz[now]=1; dep[now]=dep[fa]+1; anc[now]=fa;for (auto to:E[now]){if (to==fa) continue;DFS1(to,now); sz[now]+=sz[to];if (sz[to]>sz[son[now]]) son[now]=to;}}inline void DFS2(CI now=1,CI tf=1){seq[dfn[now]=++idx]=now; top[now]=tf;if (son[now]) DFS2(son[now],tf);for (auto to:E[now]){if (to==anc[now]||to==son[now]) continue;DFS2(to,to);}}inline void modify_path(int x,CI mv){// printf("x = %lld\n",x);while (top[x]!=top[1]){itv.emplace_back(dfn[top[x]],dfn[x]);// printf("(%lld, %lld) + %lld\n",dfn[top[x]],dfn[x],mv);SEG.modify(dfn[top[x]],dfn[x],mv);x=anc[top[x]];}// printf("(%lld, %lld) + %lld\n",dfn[1],dfn[x],mv);itv.emplace_back(dfn[1],dfn[x]);SEG.modify(dfn[1],dfn[x],mv);}inline void modify_sbt(CI x,CI mv){itv.emplace_back(dfn[x],dfn[x]+sz[x]-1);SEG.modify(dfn[x],dfn[x]+sz[x]-1,mv);}
};
using namespace Heavy_Division;
int szb[N],szg[N];
inline void DFS3(CI G,CI now=1,CI fa=0)
{szg[now]=szb[now]=0;if (a[now]) ++szg[now]; else ++szb[now];for (auto to:E[now]){if (to==fa) continue;DFS3(G,to,now);szg[now]+=szg[to];szb[now]+=szb[to];}
}
inline void solve(CI G)
{for (RI i=1;i<=n;++i)key[i]=(sz[i]==G);DFS3(G); SEG.build();// printf("key: ");// for (RI i=1;i<=n;++i)// if (key[i]) printf("%d ",i);// putchar('\n');auto update=[&](CI x,CI y,CI d){itv.clear();modify_path(x,szb[y]*d);modify_sbt(y,(G-szg[y])*d);sort(itv.begin(),itv.end());for (RI i=0;i+1<(int)itv.size();++i)assert(itv[i].second<=itv[i+1].first);int lst=1;for (auto [l,r]:itv){assert(l<=r);if (lst<=l-1){// printf("(%lld, %lld) + %lld\n",lst,l-1,szg[y]*d);SEG.modify(lst,l-1,szg[y]*d);}lst=r+1;}if (lst<=n) SEG.modify(lst,n,szg[y]*d);};for (RI i=1;i<n;++i){int x=u[i],y=v[i];if (dep[x]>dep[y]) swap(x,y);// printf("x = %lld, y = %lld, d = %lld\n",x,y,d[i]);update(x,y,w[i]);}for (RI i=1;i<=q;++i){int x=u[e[i]],y=v[e[i]];if (dep[x]>dep[y]) swap(x,y);// printf("x = %lld, y = %lld, d = %lld\n",x,y,d[i]);update(x,y,d[i]);ans[i]=min(ans[i],SEG.query());}
}
signed main()
{scanf("%lld%s",&n,s+1);int G=0,B=0;for (RI i=1;i<=n;++i)if (s[i]=='G') ++G; else ++B;for (RI i=1;i<n;++i){scanf("%lld%lld%lld",&u[i],&v[i],&w[i]);E[u[i]].emplace_back(v[i]);E[v[i]].emplace_back(u[i]);}scanf("%lld",&q);for (RI i=1;i<=q;++i)scanf("%lld%lld",&e[i],&d[i]),ans[i]=INF;DFS1(); DFS2();for (RI i=1;i<=n;++i) a[i]=(s[i]=='G');solve(G);for (RI i=1;i<=n;++i) a[i]=(s[i]=='B');solve(B);for (RI i=1;i<=q;++i) printf("%lld\n",ans[i]);return 0;
}

E. Experiments With Divine Trees

这个题队友好像也会了,同样没时间写;感觉这场我的博弈和 DS 出的慢要背大锅啊


H. Heroes and Illusions

这题纯队友讨论出来的,我也不知道做法

#include<bits/stdc++.h>
using namespace std;
#define LL long longconst int N = 1e5+5;
const int MOD = 998244353;void mul(int &x, int a) { x=1LL*x*a%MOD; }
LL sqr(int x) {return 1LL*x*x;}int n, fac[N], invf[N];
LL k;int powe(int x, int p) {int res = 1;while (p>0) {if (p&1) mul(res, x); mul(x, x); p>>=1;}return res;
}int inv(int x) {return powe(x, MOD-2);}int C(int n, int m) {return 1LL*fac[n]*invf[m]%MOD*invf[n-m]%MOD;
}int solve() {cin >> n >> k;if (0 == k) return 1;if (1LL*(n+1)*(n+1) < 4*k) return 0;LL det = sqr(n+1)-4*k;if (sqr(int(sqrtl(det))) != det) return 0;det = int(sqrtl(det));if (n+1 < det || (n+1-det)%2 != 0) return 0;int i = (n+1-det)/2;if (i == k/i) return C(n, i)%MOD;else return (C(n, i) + C(n, k/i))%MOD;return 0;
}signed main() {ios::sync_with_stdio(0); cin.tie(0);fac[0] = fac[1] = invf[0] = invf[1] = 1;for (int i=2; i<N; ++i) fac[i] = 1LL*fac[i-1]*i%MOD;invf[N-1] = inv(fac[N-1]);for (int i=N-1; i>1; --i) invf[i-1] = 1LL*invf[i]*i%MOD;int T; cin >> T; while (T--) cout << solve() << '\n';return 0;
}

J. Jumping Game

首先根据经典的图上博弈模型,考虑将图黑白染色并把能走到的点连边

先手必败的条件等价去掉起始点时剩下的图有完美匹配

不难发现当 \(r,c\) 都比较大时,偶数个点一定先手必胜(因为剩下点个数为奇数),奇数个点时也可以认为剩下部分能完美匹配

但是 \(r,c\) 较小的时候就是会有需要讨论的 Corner Case,然后我们只需要暴力打个表找出这些情况即可,\(\min(r,c)\le 2\) 的情况是 trivial 的

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,r,c,a,b;
int main()
{for (scanf("%d",&t);t;--t){scanf("%d%d%d%d",&r,&c,&a,&b);if (r>c) swap(r,c),swap(a,b);if (r==1) { puts("Brahma"); continue; }if (r==2){int left=(b-1)/2,right=(c-b)/2;if (left%2==1||right%2==1) puts("Annapurna");else puts("Brahma");continue;}if (r==3&&c==3){puts(a==2&&b==2?"Brahma":"Annapurna");continue;}if (r==3&&c==5){if ((a+b)%2==1) { puts("Annapurna"); continue; }if ((a==1&&b==3)||(a==3&&b==3)) puts("Annapurna");else puts("Brahma");continue;}if (r%2==0||c%2==0) puts("Annapurna");else puts((a+b)%2==0?"Brahma":"Annapurna");}return 0;
}

K. Kangaroo On Graph

由于图是个 DAG,因此可以拓扑排序求最短路

对于每个点 \(x\),把所有能到达它的路径权值 \(val\) 和前驱点 \(pre\) 看作二元组并排序存下来

对于其所有出边我们暴力在里面找个最优的更新即可,分析一波复杂度是线性的

#include<cstdio>
#include<iostream>
#include<set>
#include<vector>
#include<queue>
#include<array>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,INF=1e18;
typedef pair <int,int> pi;
int n,m,k,deg[N]; vector <pi> f[N];
vector <pi> v[N]; set <array <int,3>> ban;
signed main()
{scanf("%lld%lld",&n,&m);for (RI i=1;i<=m;++i){int x,y,z;scanf("%lld%lld%lld",&x,&y,&z);v[x].push_back({y,z}); ++deg[y];}scanf("%lld",&k);for (RI i=1;i<=k;++i){int a,b,c;scanf("%lld%lld%lld",&a,&b,&c);ban.insert({a,b,c});}queue <int> q; f[1].push_back({0,0});for (RI i=1;i<=n;++i) if (deg[i]==0) q.push(i);while (!q.empty()){int now=q.front(); q.pop();sort(f[now].begin(),f[now].end());for (auto [to,w]:v[now]){int val=INF;for (auto [dis,lst]:f[now]){if (ban.count({lst,now,to})) continue;val=dis; break;}if (val!=INF) f[to].push_back({val+w,now});if (--deg[to]==0) q.push(to);}}if (f[n].empty()||f[n][0].first>=INF) puts("-1"); else printf("%lld",f[n][0].first);return 0;
}

L. Low Cost Set

首先不难发现把所有的区间都扔到集合里一定是个合法解

考虑如果存在两个区间 \([a,b),[c,d)\),满足 \(a<c<b<d\)(即它们有交),那么我们可以把它们拆成 \([a,c),[c,b),[b,d)\),以此减少 \([b,c)\) 的贡献

不难发现由于拆分只能进行一次,因此两个区间配对拆分后就不能再进行操作了

把能配对拆分的区间之间连对应权值的边,转化为一般图的最大权匹配,然后去 UOJ 上抄个老哥的板子跑一跑就行

#include<bits/stdc++.h>
using namespace std;
#define all(x) (x).begin(),(x).end()
namespace weighted_blossom_tree
{#define d(x) (lab[x.u]+lab[x.v]-e[x.u][x.v].w*2)const int N=105*2;//两倍点数typedef long long ll;//总和大小typedef ll T;//权值大小//均不允许无符号const T inf=numeric_limits<int>::max()>>1;struct Q{int u,v;T w;} e[N][N];T lab[N];int n,m=0,id,h,t,lk[N],sl[N],st[N],f[N],b[N][N],s[N],ed[N],q[N];vector<int> p[N];void upd(int u,int v) {if (!sl[v]||d(e[u][v])<d(e[sl[v]][v])) sl[v]=u;}void ss(int v){sl[v]=0;for (int u=1;u<=n;u++) if (e[u][v].w>0&&st[u]!=v&&!s[st[u]]) upd(u,v);}void ins(int u) {if (u<=n) q[++t]=u; else for (int v:p[u]) ins(v);}void mdf(int u,int w){st[u]=w;if (u>n) for (int v:p[u]) mdf(v,w);}int gr(int u,int v){if ((v=find(all(p[u]),v)-p[u].begin())&1){reverse(1+all(p[u]));return (int)p[u].size()-v;}return v;}void stm(int u,int v){lk[u]=e[u][v].v;if (u<=n) return;Q w=e[u][v];int x=b[u][w.u],y=gr(u,x),i;for (i=0;i<y;i++) stm(p[u][i],p[u][i^1]);stm(x,v);rotate(p[u].begin(),y+all(p[u]));}void aug(int u,int v){int w=st[lk[u]];stm(u,v);if (!w) return;stm(w,st[f[w]]);aug(st[f[w]],w);}int lca(int u,int v){for (++id;u|v;swap(u,v)){if (!u) continue;if (ed[u]==id) return u;ed[u]=id;//??????????v?? 这是原作者的注释,我也不知道是啥if (u=st[lk[u]]) u=st[f[u]];}return 0;}void add(int u,int a,int v){int x=n+1,i,j;while (x<=m&&st[x]) ++x;if (x>m) ++m;lab[x]=s[x]=st[x]=0;lk[x]=lk[a];p[x].clear();p[x].push_back(a);for (i=u;i!=a;i=st[f[j]]) p[x].push_back(i),p[x].push_back(j=st[lk[i]]),ins(j);//复制,改一处reverse(1+all(p[x]));for (i=v;i!=a;i=st[f[j]]) p[x].push_back(i),p[x].push_back(j=st[lk[i]]),ins(j);mdf(x,x);for (i=1;i<=m;i++) e[x][i].w=e[i][x].w=0;memset(b[x]+1,0,n*sizeof b[0][0]);for (int u:p[x]){for (v=1;v<=m;v++) if (!e[x][v].w||d(e[u][v])<d(e[x][v])) e[x][v]=e[u][v],e[v][x]=e[v][u];for (v=1;v<=n;v++) if (b[u][v]) b[x][v]=u;}ss(x);}void ex(int u)  // s[u] == 1{for (int x:p[u]) mdf(x,x);int a=b[u][e[u][f[u]].u],r=gr(u,a),i;for (i=0;i<r;i+=2){int x=p[u][i],y=p[u][i+1];f[x]=e[y][x].u;s[x]=1;s[y]=0;sl[x]=0;ss(y);ins(y);}s[a]=1;f[a]=f[u];for (i=r+1;i<p[u].size();i++) s[p[u][i]]=-1,ss(p[u][i]);st[u]=0;}bool on(const Q &e){int u=st[e.u],v=st[e.v],a;if(s[v]==-1){f[v]=e.u;s[v]=1;a=st[lk[v]];sl[v]=sl[a]=s[a]=0;ins(a);}else if(!s[v]){a=lca(u,v);if (!a) return aug(u,v),aug(v,u),1;else add(u,a,v);}return 0;}bool bfs(){memset(s+1,-1,m*sizeof s[0]);memset(sl+1,0,m*sizeof sl[0]);h=1;t=0;int i,j;for (i=1;i<=m;i++) if (st[i]==i&&!lk[i]) f[i]=s[i]=0,ins(i);if (h>t) return 0;while (1){while (h<=t){int u=q[h++],v;if (s[st[u]]!=1) for (v=1; v<=n;v++) if (e[u][v].w>0&&st[u]!=st[v]){if (d(e[u][v])) upd(u,st[v]); else if (on(e[u][v])) return 1;}}T x=inf;for (i=n+1;i<=m;i++) if (st[i]==i&&s[i]==1) x=min(x,lab[i]>>1);for (i=1;i<=m;i++) if (st[i]==i&&sl[i]&&s[i]!=1) x=min(x,d(e[sl[i]][i])>>s[i]+1);for (i=1;i<=n;i++) if (~s[st[i]]) if ((lab[i]+=(s[st[i]]*2-1)*x)<=0) return 0;for (i=n+1;i<=m;i++) if (st[i]==i&&~s[st[i]]) lab[i]+=(2-s[st[i]]*4)*x;h=1;t=0;for (i=1;i<=m;i++) if (st[i]==i&&sl[i]&&st[sl[i]]!=i&&!d(e[sl[i]][i])&&on(e[sl[i]][i])) return 1;for (i=n+1;i<=m;i++) if (st[i]==i&&s[i]==1&&!lab[i]) ex(i);}return 0;}template<typename TT> ll max_weighted_general_match(int N,const vector<tuple<int,int,TT>> &edges)//[1,n],返回权值{memset(ed+1,0,m*sizeof ed[0]);memset(lk+1,0,m*sizeof lk[0]);n=m=N;id=0;iota(st+1,st+n+1,1);int i,j;T wm=0;ll r=0;for (i=1;i<=n;i++) for (j=1;j<=n;j++) e[i][j]={i,j,0};for (auto [u,v,w]:edges) wm=max(wm,e[v][u].w=e[u][v].w=max(e[u][v].w,(T)w));for (i=1;i<=n;i++) p[i].clear();for (i=1;i<=n;i++) for (j=1;j<=n;j++) b[i][j]=i*(i==j);fill_n(lab+1,n,wm);while (bfs());for (i=1;i<=n;i++) if (lk[i]) r+=e[i][lk[i]].w;return r/2;}#undef d
}
using weighted_blossom_tree::max_weighted_general_match,weighted_blossom_tree::lk;
#define RI register int
const int N=105;
int k,l[N],r[N],A[N<<1];
int main()
{scanf("%d",&k);for (RI i=1;i<=k;++i)scanf("%d%d",&l[i],&r[i]);for (RI i=1;i<2*k;++i)scanf("%d",&A[i]);long long ans=0;for (RI i=1;i<=k;++i)for (RI j=l[i];j<r[i];++j) ans+=A[j];vector<tuple<int,int,long long>> edges;for (RI i=1;i<=k;++i)for (RI j=1;j<=k;++j){if (i==j) continue;int a=l[i],b=r[i],c=l[j],d=r[j];if (a<c&&c<b&&b<d){long long sum=0;for (RI s=c;s<b;++s) sum+=A[s];edges.emplace_back(i,j,sum);}}printf("%lld",ans-max_weighted_general_match(k,edges));return 0;
}

Postscript

争取 Regional 前保持一周两训吧,能做到吗(还剩两次!)

http://www.hskmm.com/?act=detail&tid=26409

相关文章:

  • 项目作业2
  • 如何使用 INFINI Gateway 对比 ES 索引数据
  • Ambari安装Hadoop
  • Ambari-bigtop搭建hadoop数据仓库架构
  • 安装Ambari集群
  • Python中的`namedtuple`:命名元组的用法与优势
  • 我的首页
  • 一摞python风格的纸牌
  • 记录一个ubuntu24.04蓝牙不显示不可用的解决方案
  • AI时代需要重新定义投资回报评估模型
  • MOVEit网络攻击波及普华永道与安永,供应链安全再响警钟
  • shell编程
  • Penchick Online Mathematical Olympiad, Qualifying Test 1, III.4
  • QBXT2025S刷题 Day6
  • CF2145 Educational Codeforces Round 183 (Rated for Div. 2) 游记
  • 52个AI工具
  • 可观测专题【左扬精讲】——《Go 语言实现企业级 APM 监控系统实战:从 0 到 1 搭建高性能监控平台》
  • 多区域多 VLAN 网络搭建与访问控制及服务器部署实验
  • Tina_Linux_系统软件 开发指南
  • 2025方钢、扁钢、圆钢、光轴、六角钢、异型钢、冷拉/冷拔方钢、冷拉/冷拔扁钢、冷拉/冷拔圆钢、冷拉/冷拔六角钢、冷拉/冷拔异型钢、热轧方钢/扁钢厂家权威推荐榜:坚固耐用与精准定制口碑之选
  • GO_基础2
  • LDO(一)FVF型LDO
  • 详细介绍:进阶智能体实战九、图文需求分析助手(ChatGpt多模态版)(帮你生成 模块划分+页面+表设计、状态机、工作流、ER模型)
  • 09. 常用控件
  • 201007
  • 苍穹外卖第一天(Maven、Git、Nginx反向代理)
  • Python中的数据结构
  • 2025家纺摄影公司/南通摄影公司权威推荐榜:创意拍摄与专业服务的口碑之选
  • 合成数据生成技术研讨会深度解析
  • 纯 C++ 开发的 Telegram Bot 框架