普通视图
隔夜美股市场集体收涨,大型科技股全线上涨
9点1氪 | 泰国预计春节中国游客同比增长22.6%;美国最高法院维持TikTok不卖就禁法案;近期将恢复上海福建居民赴台湾团队游
上市进行时
信凯科技集团股份有限公司
36氪获悉,证监会同意浙江信凯科技集团股份有限公司首次公开发行股票并在深交所主板上市的注册申请。
TOP 3 大新闻
泰国预计春节期间近77万中国游客将入境,同比增长22.6%
泰国副总理兼交通部长素里亚16日表示,1月24日至2月2日当周,预计入境外国游客数量将超过400万人次,同比增长10.4%;其中中国游客约77万人次,同比增长22.6%。素里亚强调了服务质量和游客安全的重要性,称农历新年期间将增加机场工作人员数量。
此前曼谷邮报报道,泰国旅行社协会预计,旅游安全问题可能导致农历新年期间到访泰国的中国游客人数下降10%至20%,可能给泰国旅游业造成春节期间高达50亿泰铢(约10亿人民币)的损失。(财联社)
美国最高法院维持TikTok“不卖就禁”法案
财联社1月17日电,美国最高法院就TikTok“不卖就禁”法案做出决定:相关条款并未侵犯请愿人第一修正案权利,最高法院支持这一法案,要求TikTok在19日前从其母公司字节跳动集团剥离,否则将面临美国全国禁令。此前有消息称,美国当选总统特朗普正考虑在上任后发布一项行政命令,暂停执行TikTok禁令60至90天。
大陆将于近期恢复福建、上海居民赴台团队游
文旅部消息,为进一步促进两岸人员往来正常化和各领域交流常态化,回应台湾基层民众和旅游业界热切期盼,增进两岸同胞利益福祉,大陆方面将于近期恢复福建、上海居民赴台团队游。目前各项工作正在积极筹备中,希望两岸旅游业界加强沟通对接,为大陆居民赴台团队游提供优质的服务和产品。(央视新闻)
大公司/大事件
极越汽车:启动用户定金退款登记,1月22日起退回
36氪获悉,据极越汽车官微,自2025年1月17日起,极越汽车将针对已支付意向金/定金、尚未提车且符合退款条件用户,有序开展全额退款工作。预计1月22日起,款项会通过银行转账的方式退回至用户的账户。
SpaceX“星舰”第七次试飞,再现“筷子夹火箭”,二级飞船失联
美国太空探索技术公司(SpaceX)新一代重型运载火箭“星舰”实施第七次试飞,第二级飞船失联。马斯克随后发帖并配发视频称,“成功是不确定的,但娱乐是有保证的!”他还写道,改进版星舰和助推器已准备就绪,等待发射。SpaceX“星舰”从美国得克萨斯州发射升空不久后,火箭第二级飞船与地面团队失去联系。直播画面显示,发射后该火箭第一级助推器又一次实现发射塔回收,在降落时由发射塔上的机械臂“夹住”,在半空中“捕获”回收。但火箭第二级飞船随后与地面团队失去联系,飞船直播画面中断。
36氪获悉,据报道,SpaceX将与美国联邦航空管理局(FAA)对星舰进行彻底调查。(界面新闻、36氪)
荣耀正式官宣“换帅”:前华为悍将李健接任
1月17日,荣耀终端股份有限公司内网发布公告称:“赵明因身体原因,向公司提出辞去CEO等相关职务,董事会经过慎重讨论研究,决定尊重赵明的个人意愿,接受他的辞呈,同时决定由李健任CEO职务。”
新任CEO李健被称为前华为悍将。他于2001年加入华为,2017年起进入华为监事会,在战略管理和全球化作战等方面拥有丰富的经验,并且李健也参与过华为公司重大改革和战略制定,在诸多重要战役中有突出的领导表现。2021年李健加入新荣耀,先后任管理团队核心成员、副董事长、董事、人力资源部总裁等职务。(每经网)
国家统计局最新数据:2024年全国城镇调查失业率平均值5.1%
据国家统计局,2024年,全国城镇调查失业率平均值为5.1%,比上年下降0.1个百分点。12月份,全国城镇调查失业率为5.1%。本地户籍劳动力调查失业率为5.3%;外来户籍劳动力调查失业率为4.6%,其中外来农业户籍劳动力调查失业率为4.5%。31个大城市城镇调查失业率为5.0%。全国企业就业人员周平均工作时间为49.0小时。(南方都市报)
黄仁勋出席深圳分公司年会,发红包1万起步
1月15日晚,英伟达CEO黄仁勋出席在深圳分公司的年会,感谢了员工的努力工作。据在场的网友 CoderEstrella,黄仁勋发了数额1万和2万5的红包各十多个,最高奖4万红包发出5个。据此前的报道,谈及此行的目的,黄仁勋表示是为了和员工庆祝春节。(界面新闻)
小红书月初至今总下载量有22%来自美国,TikTok日活降至8220万
据估算,过去七天(当地时间1月8日至1月14日),小红书在美国的移动应用下载量较此前七天(当地时间1月1日至1月7日)增长超过20倍,较2024年同期增长超过30倍。月初至今,小红书应用总下载量中有超过五分之一(22%)来自美国,而2024年同期这一比例仅为2%。另外,数据还显示,2024年12月小红书在美国的下载量同比增长超过130%。
此外,截至1月13日,字节跳动旗下另一社交平台Lemon8在美国的日活跃用户达170万,高于对数周前的约110万。TikTok美国日活跃用户则按周跌2.1%,至约8220万。(每经网、金融界)
微信送礼功能正式上线,商品限额1万元
1月17日,微信正式上线了送礼物功能。当用户打开与好友的聊天窗口时,会发现一个与红包功能并列选项——送礼物。这一功能不仅占据了显著的位置,且允许用户撰写个性化的祝福语并精心挑选心仪的礼物。对于接收礼物的人来说,他们将在聊天框中直接收到礼物信息,并享有在同等价格范围内更换商品款式的权利。填写好自己收货地址后,等待礼物送达就行。
值得注意的是,微信小店在送礼功能上设定了1万元以下的商品限制,确保交易的便捷与安全。 (快科技)
英伟达黄仁勋:超算平台Blackwell生产正在爬坡,技术非常难
英伟达CEO黄仁勋在15日的深圳年会上表示, everything is so hard,but we like hard! 他表示,如果容易,我们让其他公司去做,英伟达只做困难的事情。黄仁勋透露,英伟达超算平台Blackwell正处于非常非常陡峭的斜坡,正在全面生产,但是没什么容易的事情,技术非常难。(新浪科技)
手机等三类数码产品补贴为购新补贴,无需交旧手机
1月16日下午,商务部召开专题新闻发布会,介绍手机、平板、智能手表(手环)购新补贴和家电以旧换新政策有关情况。商务部相关负责人介绍, 手机等三类数码产品补贴为购新补贴,不是以旧换新,不以交旧为前提。 手机等产品补贴采取支付立减方式增加百姓获得感。 消费者购买手机等产品时,不论是通过线上平台还是线下实体店,都可以享受支付立减,第一时间享受到补贴的实惠。 (央视新闻)
马斯克:美国加州莫斯兰丁发电站起火事故与特斯拉无关,Megapack运行良好
北京时间1月17日,特斯拉CEO马斯克在社交平台发文辟谣,称美国加利福尼亚州莫斯兰丁发电站起火事故与特斯拉无关,其Megapack运行良好。据美国媒体此前报道,美国加州一个储存有大量锂电池的发电站1月16日下午起火,因火势持续,约1500人已被要求疏散避难,加利福尼亚州北部的1号公路被部分关闭。据报道,起火地点为莫斯兰丁发电站,位于旧金山以南124公里处,隶属瑞致达能源公司。莫斯兰丁发电站储存有数以万计的锂电池。起火原因目前尚不清楚。(界面新闻)
电动汽车竞争加剧,特斯拉打折出售部分Cybertruck皮卡
根据特斯拉网站上的信息,特斯拉本周将开始对库存中的Cybertruck电动皮卡提供折扣。信息显示,新款Cybertruck的折扣最高可达1600美元,具体价格取决于配置,而库存卡车的演示版本折扣最高可达2630美元。马斯克曾表示,该公司每年可以销售50万辆Cybertruck,然而,特斯拉去年仅交付了约4万辆Cybertruck。特斯拉报告称,2024年全球共交付约179万辆汽车,较2023年创纪录的181万辆下降1.1%。这也是特斯拉自2011年以来汽车年交付量首次同比下滑。(财联社)
亚马逊在北美商店部门裁员200人,涉及时尚和健身业务员工
当地时间1月16日,亚马逊证实将在北美商店部门裁员约200人。商店部门为核心零售业务,涵盖包括自有品牌、Prime会员计划和消费品业务。此次裁员包括时尚和健身业务等领域的员工。(界面新闻)
资生堂回应中国市场是否涨价
近日,日本资生堂集团官网发布“价格调整通知”称,自2025年4月17日(星期四)起,由于原材料成本上涨,资生堂将对部分产品价格进行调整。目前,在资生堂中国官网未发现有相关涨价通知。17日,资生堂中国表示,中国市场的建议零售价由品牌基于品牌、产品战略和市场情况决定。(中新经纬)
X在亚特兰大的数据中心服务器出现问题
据报道,马斯克表示,X在亚特兰大的数据中心服务器出现了问题。(财联社)
铃木将集中在印度生产纯电动汽车
据报道,正在印度访问的铃木公司社长铃木俊宏1月16日在首都新德里接受媒体采访时表示:“对铃木而言,印度的强项在于规模效益。集中在这里以高效的方式生产纯电动汽车(EV)非常合适。”他表示,现阶段将把EV生产重点放在印度。(界面新闻)
施罗德据称将裁员3%,涉及约200名员工
施罗德计划裁员至多3%,新任首席执行官Richard Oldfield寻求重振这家英国最大的独立资产管理公司。据一位知情人士称,施罗德将裁员约200人,其中大部分是在技术部门。因谈论未公开事项,此人要求匿名。这是Oldfield在11月份上任以来的首批重大行动之一。总部伦敦的这家公司发言人在声明中表示,“我们的首要任务是在向增长转型的过程中同步重新调整业务定位。”(新浪财经)
地平线组织再调整,苏箐将主导高阶智驾方案落地
国产智能驾驶解决方案提供商地平线近期发生多项组织调整,主要涉及智能汽车事业部、“土星五号”项目团队等。调整完成后,地平线高阶智驾研发、工程团队的管理权限,将进一步向副总裁兼首席架构师苏箐集中。(晚点Auto)
AI最前线
将AI助手Copilot纳入Word、Excel、PPT,微软Office 12年来首次在美涨价
微软1月16日宣布,将旗下AI助手Copilot嵌入包括Word、Excel、PowerPoint、Outlook和OneNote在内的一系列Office应用程序,同时将原名Office 365的Microsoft 365每月订阅价格上调3美元,对新订阅者立即生效。这是该软件套件自2013年上市以来首次涨价。(界面新闻)
腾讯AI助手“元宝”团队调整至CSIG,腾讯会议负责人带队
36氪获悉,近期腾讯AI助手应用“元宝”完成了一次组织调整,产品团队整体调整至到CSIG(云与智慧产业事业群)。调整后,“元宝”应用将交由腾讯会议负责人吴祖榕(Lori Wu)负责。吴祖榕是从0到1孵化腾讯会议的主要角色之一。腾讯会议自2020年上线后,如今用户已经超过3亿,是企业服务市场的现象级产品。此后,吴祖榕将会主要负责元宝的产品能力建设和体验优化。腾讯混元大模型研发团队,则会继续专注模型能力和性能提升,为“元宝”提供技术支持。
三星正着手与OpenAI合作开发AI电视
据报道,三星电子正在与OpenAI协调,在两家公司之间建立“开放式合作伙伴关系”。如果与OpenAI的合作关系最终确定,预计将推出各种人工智能驱动的服务,打造新一代人工智能电视。(财联社)
投融资
“迈捷生命科学”完成数千万元A+轮融资
36氪获悉,近日,“迈捷生命科学”顺利完成由元生创投独家投资的数千万元A+轮融资。本轮融资是公司在近2个月内的第二次融资,公司前两轮累计融资额近亿元。本轮融资将进一步补充公司的流动资金,用于公司目前医美产品的临床推进,以及骨科创新产品上市后的销售推广。
“诚芯智联”完成新一轮融资
36氪获悉,近日,“诚芯智联”成功获得力合资本、芯汇投资两大投资机构的融资支持。诚芯智联将进一步加大在技术研发、人才培养、产能扩建与市场拓展等方面的投入,不断扩大市场份额,提升品牌影响力。
“元视芯”完成A轮数亿元融资
36氪获悉,近日,“元视芯”成功完成A轮数亿元融资。本轮融资由开源证券、西投控股引领,江诣创投、祥峰投资、GRC富华资本、经发资产等多家机构联合投资,为元视芯车规芯片的量产提供了必要的资金支持。
酷产品
Switch2确认兼容初代游戏,预告新一代马力欧赛车游戏
1月16日,任天堂正式公布了 Switch 2 游戏机,将于4月2日举行直面会公布更多细节信息。在官宣视频中,任天堂确认Switch 2支持原版Switch的游戏卡带,也就是说可以降低老玩家的游戏成本(可能不支持部分游戏,详情将在任天堂后续官网页面公布)。
此外,任天堂还预告了其下一代《马力欧赛车》游戏,不过任天堂目前尚未公布游戏的具体名称和细节。
据悉,距离上一代《马力欧赛车》的推出已经过去了相当长的时间。此外,任天堂还在 2019 年推出了一款名为《马力欧赛车 Tour》的手机衍生游戏,但该游戏在2023年停止了更新(IT之家)
iPhone SE 4机模再曝光,或在2025年3、4月发布
据IT之家消息,消息源1月16日在X平台发布推文,分享了苹果iPhone SE 4(或叫iPhone 16E)的机模照片,共有黑色和白色两种选项,整体外观类似于iPhone 14,取消Home按键,配备6.1英寸OLED屏幕。消息称苹果iPhone SE 4将配备和iPhone 16系列相同的A18芯片,可能会首次采用苹果自主研发的5G调制解调器。 相机方面, 配备4800万像素后置摄像头,相比现有型号的1200万像素传感器有了大幅提升 。
苹果iPhone SE 4会统一端口配置,将Lightning接口更换为USB-C接口。这一改变符合欧盟法规,并简化了已经拥有USB-C配件用户的充电和数据传输。此外,iPhone SE 4可能会支持MagSafe,用户可以吸附充电器和配件,更加便捷。苹果可能会在2025年3/4月发布iPhone SE 4手机,预计起售价为499美元(当前约3662元人民币)。(IT之家)
整理|王欣逸
本文来自微信公众号“36氪”,36氪经授权发布。
上海航交所:市场临近假期 运价继续下跌
荣耀 CEO 赵明发文宣布离职/制糖工厂发布 80Gbps 细雳线/华为手机计划重返全球市场
制糖工厂发布 80Gbps 细雳线,极致体验再升级
荣耀 CEO 赵明发长文宣布离职
新款 HomePod 或配备 7 英寸 LCD 屏幕
OpenAI CEO:o3-mini 已完成,即将上线
华为手机计划重返全球市场
OpenAI 前 CTO 创业,挖走前司高管
极越汽车启动用户定金退款登记
雷诺在上海成立研发中心,专门开发欧洲车型
MiniMax CEO:千万别套用移动互联网的逻辑来做 AI
比亚迪汉 L、唐 L 正式亮相
影石 Insta 360 发布新一代手机稳定器 Flow 2 Pro
喜茶成都首家 DP 店开业,带来限定炒冰
腾讯推出 AI 检测工具
《花样年华》史上最长版本将全球首映
美国著名导演大卫·林奇去世
周末也值得一看的新闻
制糖工厂发布 80Gbps 细雳线,极致体验再升级
制糖工厂CANDYSIGN 今日推出 80Gbps 细雳线,这是继去年 7 月发布 40Gbps 细雳线后的又一颠覆性探索,让极致的体验再度升级。
它兼容雷电 5,传输速度达到 80Gbps,一秒约传 6GB,真正实现一日拍摄一秒传输,对于户外拍摄的创作者十分友好。同时,它也是 Mac mini 飙配,是不可缺少的生产力搭档。80Gbps 细雳线还支持 240W 旗舰快充,搭配制糖工厂最新推出的 AI 小电拼,多口飙充快速补能。
与市面上诸多全功能线不同,80Gbps 细雳线拥有极细线径,且延续着贴贴线家族的全磁吸设计,自卷自吸自收纳,随身携带或桌面使用,都能整洁不乱。
超细超软超犀利,相信能带给各位更极致的体验。
荣耀前 CEO 赵明发长文宣布离职
1 月 17 日,荣耀前 CEO 赵明在微博发文,宣布正式离职。
文中,赵明表明是因高强度的工作导致身体情况不佳而选择离职。赵明也透露,未来他将规划修复自身的健康状况和多陪伴家人,并感慨「以后不会再有明哥发布会名场面了」。
赵明表示,自己毕业就进入华为,其中在荣耀奋斗了 10 年,工作共计 27 年,并表示荣耀几乎就是其生活的全部。赵明在文中还提到,其股票还在荣耀中,因此也期待 2025 年荣耀能够更上一层楼。
同日,据每日经济报道,荣耀终端股份有限公司内网发布公告称,赵明向公司提出辞去 CEO 等相关职务,董事会经过慎重讨论研究,决定尊重赵明的个人意愿,接受他的辞呈,同时决定由李健任 CEO 职务。
据悉,新任 CEO 李健被称为前华为悍将,其于 2001 年加入华为,2017 年起进入华为监事会,在战略管理和全球化作战等方面拥有丰富的经验,并且李健也参与过华为公司重大改革和战略制定,在诸多重要战役中有突出的领导表现。
2021 年,李健加入新荣耀,先后任管理团队核心成员、副董事长、董事、人力资源部总裁等职务。
新款 HomePod 或配备 7 英寸 LCD 屏幕
1 月 17 日,据 DIGITIMES 报道,苹果将会在 2025 年推出一款配备显示屏的 HomePod。
据悉,带屏版 HomePod 将搭载一块 7 英寸的 LCD 显示器,并非搭载此前传闻的 OLED 显示屏;天马微电子将成为该款 HomePod 的独家屏幕供应商,光宝科技将负责背光模组的生产。
据多方消息报道,带屏版 HomePod 将搭载苹果 A18 处理器,并支持 Apple Intelligence;比亚迪也被确认,将成为该款 HomePod 的独家组装合作伙伴。报道还指出,天马所提供的 HomePod 显示面板,每块仅需 10 美元。
同时,有行业内部人士透露,Radiant Optoelectronics 成为带屏版 HomePod 背光模组的独家供应商。据了解,Radiant 为苹果 LCD 版 iPad 的面背光供应商,此前 Radiant 为 iPad mini 提供 7 英寸背光模组,因此有足够的生产条件去供应本次带屏版 HomePod 的背光模组。
此外报道称,本次推出的带屏版 HomePod 原计划于 2024 年初发布,但遭遇多次推迟,第一次推迟至 2024 年底,现又推迟至 2025 年 3 月,而最近的消息表明,该新品或将进一步推迟至 2025 年下半年。
OpenAI CEO:o3-mini 已完成,即将上线
今日凌晨,OpenAI CEO Sam Altman 发文表示,o3-mini 已经完成,并计划未来几周内推出。
文中,Sam Altman 表示感谢测试 o3-mini 的外部安全研究人员,并且 o3-mini 已经完成了一个版本,开始着手发布流程。Altman 还透露,o3-mini 计划在未来几周内推出,同时还将推出 API 和 ChatGPT。
随后,Altman 还在评论区回复了网友的提问。性能方面,Altman 表示 o3-mini 对比 o1 Pro 会差一点,但速度将会更快;同时,o3-mini 的信息发送率将高于 o1,并且 Altman 透露将会非常高;o3-mini 也将为 Plus 用户服务。
此外,有网友提问是否能透露关于 GPT-5 的信息时,Altman 反问「想知道什么」,同时透露在 2025 年 GPT 系列模型和 o 系列模型可能会合并。
华为手机计划重返全球市场
近日,据日经新闻指出,华为或将计划重返全球市场。
据报道,2024 年底,在世界各地的大城市,出现了华为的最新款可折叠智能手机「Mate X6」的巨型广告,其中悬挂在了香港中心的主要街道、迪拜的主干道旁等明显广告位。报道指出,上述情况预示,华为或将着眼于回归全球市场并重回世界首位的目标。
据悉,华为 Mate X6 于 2024 年 12 月在中国大陆和马来西亚上市,2025 年 1 月 8 日在香港上市;最近即将在欧洲多个国家开始销售。根据华为面向各地区的官网进行统计,进驻的国家和地区包括中东和南美等在内超过 30 个。
报道还指出,虽受外部限制,但华为自研的麒麟芯片及系统都取得重要突破,目前其已不再依赖高通旗下产品。据了解,自华为 Mate 60 系列推出以来,华为的出货量再次位居中国大陆手机市场前列;而华为于 2024 年 11 月 26 日发布的 Mate 70 系列,已全部采用自研的麒麟芯片。
此外,据分析机构 Canalys 于 1 月 16 日公布的 2024 年中国智能手机出货量报告显示,华为手机出货量排名第二,达到 4,600 万台,同比增长 37%。
OpenAI 前 CTO 创业,挖走前司高管
此前,OpenAI 前 CTO Mira Murati 于 2024 年 9 月宣布离开 OpenAI,此后创立了自己的初创 AI 公司。
近日,据 Wired 报道,目前 Mira Murati 的公司开始招聘,其中 OpenAI 特别项目负责人 Jonathan Lachman 也将加入。
报道指出,Mira Murati 迄今为止,已从前公司 OpenAI、Character AI 和 Google 旗下的 Deepmind 等竞争对手公司中,挖走了大约 10 名研究人员和工程师。据知情人士透露,目前 Mira Murati 的公司还未起名,也没有明确的产品方向。
而 OpenAI 的一名发言人也在近期,确认了 Jonathan Lachman 的离职。
据悉,在离职前,Jonathan Lachman 为 OpenAI 的特别项目负责人,其工作内容为规划全公司范围的计划、维护战略合作伙伴的关系以及主持特别项目,并且直接向 OpenAI CEO Sam Altman 直接汇报。
极越汽车启动用户定金退款登记
1 月 17 日,极越汽车在第六次用户关心的问题说明中,宣布即日起,将针对已支付意向金/定金、尚未提车且符合退款条件用户,有序开展全额退款工作。
官方表示,收到该通知的用户需要尽快扫描公告中的二维码进行退款登记。此后极越将会对登记信息进行内部审核,确认完毕后将按顺序制作《汽车销售合同之终止协议》,并通过短信方式向用户发送文件链接进行签署。后期退款也将按照签署的时间进行排序。
极越在公告中也明确了退款时间,预计 2025 年 1 月 22 日起,款项会通过银行转账的方式退回至用户的账户。退款完成后,极越汽车还将保留所有退款文件和通信记录作为备案,以备未来可能会面临查询或核查。
雷诺在上海成立研发中心,专门开发欧洲车型
近日,据新华社报道,法国汽车制造商雷诺集团 CEO 卢卡·德·梅奥 15 日在接受采访时表示,日前雷诺集团在上海组建了约 150 人的研发中心,开发专供欧洲市场的电动汽车,包括新一代纯电动车型 Twingo E-Tech。
梅奥表示,中国电动汽车产业在研发速度、成本和技术上有着绝对的竞争力,与中国的制造商合作是欧洲汽车制造商的关键。通过在上海设立研发中心,雷诺集团希望将这些优势融入到欧洲市场的产品开发中,提升自身的全球竞争力。
据悉,雷诺在中国建立的这个研发中心的职能包括工程、采购、成本核算等,主要在中国开展市场分析、构建供应商体系,以及车辆及系统开发,助力雷诺集团加速产品开发进程。
梅奥透露,由于与中国的汽车产业建立了合作,雷诺集团的新一代纯电动车型 Twingo E-tech 的开发时间将只需要原计划的三分之一。
据了解,雷诺集团成立于 1898 年;2023年,雷诺集团实现销量 2,235,345 辆,位列法国第一、欧洲第三。
MiniMax CEO:千万别套用移动互联网的逻辑来做 AI
近日,MiniMax CEO 闫俊杰接受晚点采访,并表示千万别套用移动互联网的逻辑来做 AI。
闫俊杰提到,移动互联网的逻辑认为用户越多,产品迭代速度越快,但在 AI 领域,这个逻辑并不成立。并且他表示复杂任务的解决更多依赖技术突破,而非大规模用户行为数据。闫俊杰还认为,AI 模型在很多场景中比普通用户更「聪明」,大部分用户的使用并不足以推动模型进一步改进。
闫俊杰还预测,Agent 很快还会看到一类应用—信息的获取,从而让用户能更好地使用 Agent 去获取信息。同时他对比了移动互联网产品和 AI:移动互联网产品,要思考有哪些供给、哪些消费;而 AI 产品其实不需要人为供给,AI 既有分发,也有供给能力,而且 AI 能力会不停变化。
此外,闫俊杰还提及了模型开源。他认为,大模型都应该开源,并表示所有模型一年之后都会落后。同时他认为,如 OpenAI,它的核心能力已不再是对比,而是 ChatGPT 的品牌和心智。
比亚迪汉 L、唐 L 正式亮相
1 月 17 日,比亚迪汉 L、唐 L 设计发布会今日举行,两款最新车型也一同亮相。
据悉,汉 L 与唐 L 将会与现款汉、唐一同销售,两款新车定位为 30 万元级旗舰车型。
两款新车在外观方面都采用了家族式「龙颜美学」,均升级为「Loong Face」。其中,汉 L 前脸配备主动式通风口,同时翼子板上的「龙须」设计与隐藏式前门把手在视觉上形成一体。
尺寸方面,汉 L 长宽高为 5,050mm/1,960mm/1,505mm,轴距为 2,970mm;唐 L 长宽高为 5,040mm/1,996mm/1,760mm,轴距为 2,950mm。并且唐将采用 2+3+2 的 7 座布局。
动力方面,两款新车均提供纯电(EV)和插混(DM)两种动力形式。EV 版本将基于「超级 e 平台」,搭载全新刀片电池,单电机最大 500kW 功率(670 马力),双电机总功率来到 810kW(1086 马力),其中后电机的峰值功率达到 580kW(777 马力);DM 版本将搭载最新的第五代 DM 技术,1.5T 发动机最大 115kW,电机最大功率为 200kW,同时提供双电机版本的 DM-p 车型,提供 145km、170km 的纯电续航。
内饰方面,汉 L、唐 L 均采用 3D 竹木作为全新内饰材料,竹木饰板的点缀,为车内增添了几分雅致。仪表板位置采用了皮革+针织布工艺,触摸质感更柔和;扶手箱内侧也用上了植绒工艺,触感显著提升。
此外,新车的更多信息将于今年 3 月公布。
徕卡 SL3-S 全画幅无反相机发布
近日,徕卡 SL3-S 全画幅无反相机正式发布。
传感器方面,新机配备了一块 24.6MP 全画幅传感器,支持相位对焦;搭载 Maestro IV 处理器,ISO 范围为 50 至 200,000,拥有 15 档动态范围;此外,还支持 48MP、96MP 的高像素合成模式;机身最高连拍速度为 30FPS。
显示方面,新机配备了一块支持翻折的 3.2 英寸屏幕;取景器为 576 万像素,支持 120 帧。
视频规格方面,徕卡 SL3-S 最高支持内 5.9K 30 帧或 C4K 60 帧 ProRes 422 HQ 规格视频;同时,新机还支持 6K OpenGate 模式拍摄。
接口方面,机身左侧配备时间码接口、3.5mm 输入/输出接口、HDMI 2.1 接口和 USB-C 3.1 Gen2 接口。机身右侧配备双卡槽,支持 UHS-Ⅱ SD/SDHC/SDXC 及 CFexpress Type B;机内配备 8GB 存储空间,可使用外接 SSD 进行录制。
徕卡 SL3-S 机身重量为 768g;支持 IP54 级别防滴溅。新机售价为 39,200 元。
影石 Insta 360 发布新一代手机稳定器 Flow 2 Pro
1 月 16 日,影石 Insta360 发布新一代旗舰 AI 手机稳定器 Flow 2 Pro。
Flow 2 Pro 搭载了影石自研的AI深度追踪 4.0 技术,可实现动态长焦追踪、智能多人追踪、智能美学构图,还具备 360° 水平无限位追踪、自由俯仰模式等多种功能。机身采用三轴增稳和小巧便携一体化设计,内置自拍杆和三脚架,可随时随地一步开拍。
同时,Flow 2 Pro 还独家接入苹果 DockKit 技术,能在 iPhone 原生相机和 200+ 主流 iOS APP 内自动追踪人像。
喜茶成都首家 DP 店开业,带来限定炒冰
1 月 17 日,喜茶成都春熙路 DP 店「叠院」正式开业。
「叠院」是喜茶在春熙路中心开出的成都首家 DP 门店,也是喜茶西南首家炒冰店。
同时,喜茶人气「手炒·茶冰」系列也首次走出深圳,在「叠院」内限定提供三款经典产品和一款新品的同时,还推出了首款成都城市限定「厚抹蒙顶绿手炒冰」。
腾讯推出 AI 检测工具
1 月 17 日,腾讯宣布上线 AI 「鉴别工具」。而腾讯混元安全团队旗下的朱雀实验室,推出了朱雀大模型检测网站。
在这个网站里,用户可以使用这个工具对文本和图像进行 AI 检测。
官方表示,AI 生成文本检测基于多种先进的人工智能模型,构造数百万级别的数据进行训练,能够识别出人类和 AI 的书写模式。该系统不仅具备优秀的英文检测能力,在处理中文数据方面表现尤为出色。
而 AI 生成图像检测利用先进的 AI 模型检测图片是否由 AI 模型生成,或是否是真实图像。该模型经过百万张自然图片和生成图片训练,涵盖摄影、艺术、绘画等内容。可检测多类主流文生图模型生成图片,更多模型生成图片的检测持续新增中。
国家版权局等四部门启动院线电影版权保护专项工作
1 月 17 日,国家版权局在表示按照国家版权局《关于开展院线电影版权保护专项工作的通知》《关于进一步加强互联网传播作品版权监管工作的意见》及版权重点监管工作计划,根据相关权利人上报的作品授权情况,公布 2025 年度第一批重点作品版权保护预警名单。
官方表示,相关网络服务商应对版权保护预警名单内的重点作品采取以下保护措施:
- 直接提供内容的网络服务商未经许可不得提供版权保护预警名单内的作品
- 提供存储空间的网络服务商应当禁止用户上传版权保护预警名单内的作品
- 相关网络服务商应当加快处理版权保护预警名单内作品权利人关于删除侵权内容或断开侵权链接的通知
《花样年华》史上最长版本将全球首映
电影花样年华导演特别版官方微博宣布,电影花样年华 25 周年导演特别版,将以 4K 高清修复的方式,于今年 2 月 14 日在院线上映。
《花样年华》是一部于 2000 年上映的爱情浪漫电影,灵感来自刘以鬯的小说《对倒》。电影由王家卫,梁朝伟、张曼玉共同主演。
美国著名导演大卫·林奇去世
当地时间 1 月 16 日,经其家人证实,美国著名超现实主义导演大卫·林奇(David Lynch)去世,终年 78 岁。
大卫·林奇曾指导、制作出多部经典好莱坞电影和剧集,代表作品有《穆赫兰道》《蓝丝绒》和《双峰》等。
是周末啊!
One Fun Thing |红魔 X Golden Saga 臻金传奇限量典藏版
红魔 X GoldenSaga 臻金传奇限量典藏版于 1 月 17 日 10 点正式开售,配置方面,该手机采用了镀金散热 VC。
官方表示,该款手机还搭载有行业首创的碳纤维散热,手机后盖采用了定制蓝宝石玻璃材质,机身还有各类镀金细节加以点缀。
根据官网信息显示,目前该款手机仅提供 24GB + 1TB 版本,售价为 9,699 元。
周末看什么 |《沙丘》1984 年版
《沙丘》(1984 年版)是美国由大卫·林奇、弗兰克·赫伯特编剧,大卫·林奇执导,凯尔·麦克拉克伦、帕特里克·斯图尔特、何塞·费勒、肖恩·杨等主演的科幻片。
电影于 1984 年 12 月 1 日在美国上映,讲述了在遥远的未来,宇宙的帝王沙丹四世因害怕表弟李托亚崔迪公爵威胁政权而将其流放,并最终将其杀害;表弟的儿子保罗由此展开复仇之旅的故事。
买书不读指南 |《设计中的设计》
《设计中的设计》是由日本著名设计师原研哉(Kenya Hara)创作的一本关于设计哲学与实践的书籍,首次出版于 2006 年。
在这本书中,原研哉在书中探讨了设计的本质和意义。他认为,设计不仅是对功能和美学的追求,更是一种关注生活、洞察人类行为和文化的过程。设计应为人类提供一种舒适的、充满智慧的生活方式,而非单纯的视觉装饰。
同时,他还在书中提出了「无设计」与「空设计」的概念。
游戏推荐 |《逃脱学院》
该游戏由 Coin Crew Games 开发,iam8bit 发行。
在《逃脱学院》的设定中,玩家来到了一所旨在将学生训练成终极密室逃脱大师的学校。游戏中有十几个精心制作的密室,全都由现实世界中经验丰富的密室逃脱专家精心设计。
游戏支持单人作战与好友组队模式,同时支持局域网联机和联网游玩。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
FreeBuf周报 | Azure AI被黑客越狱;Windows远程桌面网关出现重大漏洞
各位 Buffer 周末好,以下是本周「FreeBuf周报」,我们总结推荐了本周的热点资讯、安全事件、一周好文和省心工具,保证大家不错过本周的每一个重点!
求出数组中最大序列值
方法一:动态规划
思路
长度为 $2k$ 的序列的值的计算方式为,先将其拆成长度相同的前后子串,然后分别计算各子串的所有元素的按位或的结果,再将两个按位或的结果进行按位异或。为了求出 $\textit{nums}$ 的长度为 $2k$ 的子序列的最大值,可以将 $\textit{nums}$ 分成左右两半,分别求出每一半中,长度为 $k$ 的子序列的按位或的所有可能性,再两两求异或的最大值。这样的分法一共有 $n-2k+1$ 种,其中 $n$ 为数组 $\textit{nums}$ 的长度。并且我们可以用动态规划的方法来求出长度为 $k$ 的子序列的按位或的结果的所有可能性。
记 $\textit{dp}[i][j]$ 为哈希集合,表示 $\textit{nums}[0...i]$ 中长度为 $j$ 的自序列按位或的所有可能性。$j \leq k$,且当 $j$ 为 $0$ 时,$\textit{dp}[i][j]$ 为空集。又根据题目限制,$\textit{nums}$ 元素的值为 $0$ 到 $127$,因此 $\textit{dp}[i][j]$ 最多包含 $128$ 个元素。进行转移时,$\textit{dp}[i][j]$ 首先会包含 $\textit{dp}[i-1][j]$ 的所有元素,表示不选取 $\textit{nums}[i]$。还可以将 $\textit{dp}[i-1][j-1]$ 中的所有元素与 $\textit{nums}[i]$ 进行按位或,将结果添加进 $\textit{dp}[i][j]$,表示选取$\textit{nums}[i]$。我们可以将上述步骤抽象成一个函数,并进一步降低空间复杂度,只返回值 $j=k$ 时的 $dp$值,可以算出左一半的长度为 $k$ 的子序列的按位或的所有可能性。再将 $\textit{nums}$ 进行逆序后再次应用这个函数,即可计算出右一半的的长度为 $k$ 的子序列的按位或的所有可能性。再两两求异或的最大值返回。
我们可以将
代码
###Python
class Solution:
def maxValue(self, nums: List[int], k: int) -> int:
def findORs(nums: List[int], k: int) -> List[set[int]]:
dp = []
prev = [set() for _ in range(k + 1)]
prev[0].add(0)
for i, num in enumerate(nums):
for j in range(min(k - 1, i + 1), -1, -1):
for x in prev[j]:
prev[j + 1].add(x | num)
dp.append(prev[k].copy())
return dp
A = findORs(nums, k)
B = findORs(nums[::-1], k)
mx = 0
for i in range(k - 1, len(nums) - k):
mx = max(mx, *(a ^ b for a in A[i] for b in B[-i - 2]))
return mx
###Java
class Solution {
public int maxValue(int[] nums, int k) {
List<Set<Integer>> A = findORs(nums, k);
List<Set<Integer>> B = findORs(reverse(nums), k);
int mx = 0;
for (int i = k - 1; i < nums.length - k; i++) {
for (int a : A.get(i)) {
for (int b : B.get(nums.length - i - 2)) {
mx = Math.max(mx, a ^ b);
}
}
}
return mx;
}
private List<Set<Integer>> findORs(int[] nums, int k) {
List<Set<Integer>> dp = new ArrayList<>();
List<Set<Integer>> prev = new ArrayList<>();
for (int i = 0; i <= k; i++) {
prev.add(new HashSet<>());
}
prev.get(0).add(0);
for (int i = 0; i < nums.length; i++) {
for (int j = Math.min(k - 1, i + 1); j >= 0; j--) {
for (int x : prev.get(j)) {
prev.get(j + 1).add(x | nums[i]);
}
}
dp.add(new HashSet<>(prev.get(k)));
}
return dp;
}
private int[] reverse(int[] nums) {
int[] reversed = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
reversed[i] = nums[nums.length - 1 - i];
}
return reversed;
}
}
###C#
public class Solution {
public int MaxValue(int[] nums, int k) {
var A = FindORs(nums, k);
Array.Reverse(nums);
var B = FindORs(nums, k);
int mx = 0;
for (int i = k - 1; i < nums.Length - k; i++) {
foreach (int a in A[i]) {
foreach (int b in B[nums.Length - i - 2]) {
mx = Math.Max(mx, a ^ b);
}
}
}
return mx;
}
public List<HashSet<int>> FindORs(int[] nums, int k) {
var dp = new List<HashSet<int>>();
var prev = new List<HashSet<int>>();
for (int i = 0; i <= k; i++) {
prev.Add(new HashSet<int>());
}
prev[0].Add(0);
for (int i = 0; i < nums.Length; i++) {
for (int j = Math.Min(k - 1, i + 1); j >= 0; j--) {
foreach (int x in prev[j]) {
prev[j + 1].Add(x | nums[i]);
}
}
dp.Add(new HashSet<int>(prev[k]));
}
return dp;
}
}
###C++
class Solution {
public:
int maxValue(vector<int>& nums, int k) {
auto findORs = [&](const vector<int>& nums, int k) -> vector<unordered_set<int>> {
vector<unordered_set<int>> dp;
vector<unordered_set<int>> prev(k + 1);
prev[0].insert(0);
for (int i = 0; i < nums.size(); ++i) {
for (int j = min(k - 1, i + 1); j >= 0; --j) {
for (int x : prev[j]) {
prev[j + 1].insert(x | nums[i]);
}
}
dp.push_back(prev[k]);
}
return dp;
};
vector<unordered_set<int>> A = findORs(nums, k);
vector<unordered_set<int>> B = findORs(vector<int>(nums.rbegin(), nums.rend()), k);
int mx = 0;
for (size_t i = k - 1; i < nums.size() - k; ++i) {
for (int a : A[i]) {
for (int b : B[nums.size() - i - 2]) {
mx = max(mx, a ^ b);
}
}
}
return mx;
}
};
###Go
func maxValue(nums []int, k int) int {
findORs := func(nums []int, k int) []map[int]bool {
dp := make([]map[int]bool, 0)
prev := make([]map[int]bool, k + 1)
for i := 0; i <= k; i++ {
prev[i] = make(map[int]bool)
}
prev[0][0] = true
for i := 0; i < len(nums); i++ {
for j := min(k - 1, i + 1); j >= 0; j-- {
for x := range prev[j] {
prev[j + 1][x | nums[i]] = true
}
}
current := make(map[int]bool)
for key := range prev[k] {
current[key] = true
}
dp = append(dp, current)
}
return dp
}
A := findORs(nums, k)
reverse(nums)
B := findORs(nums, k)
mx := 0
for i := k - 1; i < len(nums) - k; i++ {
for a := range A[i] {
for b := range B[len(nums) - i - 2] {
if a ^ b > mx {
mx = a ^ b
}
}
}
}
return mx
}
func reverse(nums []int) {
for i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 {
nums[i], nums[j] = nums[j], nums[i]
}
}
###C
typedef struct {
int key;
UT_hash_handle hh;
} HashItem;
HashItem *hashFindItem(HashItem **obj, int key) {
HashItem *pEntry = NULL;
HASH_FIND_INT(*obj, &key, pEntry);
return pEntry;
}
bool hashAddItem(HashItem **obj, int key) {
if (hashFindItem(obj, key)) {
return false;
}
HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
pEntry->key = key;
HASH_ADD_INT(*obj, key, pEntry);
return true;
}
void hashFree(HashItem **obj) {
HashItem *curr = NULL, *tmp = NULL;
HASH_ITER(hh, *obj, curr, tmp) {
HASH_DEL(*obj, curr);
free(curr);
}
}
void findORs(int* nums, int numsSize, int k, HashItem** dp) {
HashItem *prev[k + 1];
for (int i = 0; i <= k; i++) {
prev[i] = NULL;
}
hashAddItem(&prev[0], 0);
for (int i = 0; i < numsSize; ++i) {
for (int j = fmin(k - 1, i + 1); j >= 0; --j) {
for (HashItem *pEntry = prev[j]; pEntry; pEntry = pEntry->hh.next) {
int x = pEntry->key;
hashAddItem(&prev[j + 1], x | nums[i]);
}
}
for (HashItem *pEntry = prev[k]; pEntry; pEntry = pEntry->hh.next) {
int x = pEntry->key;
hashAddItem(&dp[i], x);
}
}
for (int i = 0; i <= k; i++) {
hashFree(&prev[i]);
}
};
void reverse(int *nums, int numsSize) {
for (int i = 0, j = numsSize - 1; i < j; i++, j--) {
int x = nums[i];
nums[i] = nums[j];
nums[j] = x;
}
}
int maxValue(int* nums, int numsSize, int k) {
HashItem *A[numsSize];
HashItem *B[numsSize];
for (int i = 0; i < numsSize; i++) {
A[i] = B[i] = NULL;
}
findORs(nums, numsSize, k, A);
reverse(nums, numsSize);
findORs(nums, numsSize, k, B);
int mx = 0;
for (size_t i = k - 1; i < numsSize - k; ++i) {
for (HashItem *pEntry = A[i]; pEntry; pEntry = pEntry->hh.next) {
int a = pEntry->key;
for (HashItem *pEntry = B[numsSize - i - 2]; pEntry; pEntry = pEntry->hh.next) {
int b = pEntry->key;
mx = fmax(mx, a ^ b);
}
}
}
for (int i = 0; i < numsSize; i++) {
hashFree(&A[i]);
hashFree(&B[i]);
}
return mx;
}
###JavaScript
var maxValue = function(nums, k) {
function findORs(nums, k) {
const dp = [];
const prev = Array.from({ length: k + 1 }, () => new Set());
prev[0].add(0);
for (let i = 0; i < nums.length; i++) {
for (let j = Math.min(k - 1, i + 1); j >= 0; j--) {
for (const x of prev[j]) {
prev[j + 1].add(x | nums[i]);
}
}
dp.push(new Set(prev[k]));
}
return dp;
}
const A = findORs(nums, k);
nums.reverse();
const B = findORs(nums, k);
let mx = 0;
for (let i = k - 1; i < nums.length - k; i++) {
for (const a of A[i]) {
for (const b of B[nums.length - i - 2]) {
mx = Math.max(mx, a ^ b);
}
}
}
return mx;
};
###TypeScript
function maxValue(nums: number[], k: number): number {
function findORs(nums: number[], k: number): Set<number>[] {
const dp: Set<number>[] = [];
const prev: Set<number>[] = Array.from({ length: k + 1 }, () => new Set());
prev[0].add(0);
for (let i = 0; i < nums.length; i++) {
for (let j = Math.min(k - 1, i + 1); j >= 0; j--) {
for (const x of prev[j]) {
prev[j + 1].add(x | nums[i]);
}
}
dp.push(new Set(prev[k]));
}
return dp;
}
const A = findORs(nums, k);
nums.reverse();
const B = findORs(nums, k);
let mx = 0;
for (let i = k - 1; i < nums.length - k; i++) {
for (const a of A[i]) {
for (const b of B[nums.length - i - 2]) {
mx = Math.max(mx, a ^ b);
}
}
}
return mx;
};
###Rust
use std::collections::HashSet;
use std::cmp::{max, min};
impl Solution {
pub fn max_value(nums: Vec<i32>, k: i32) -> i32 {
fn find_ors(nums: &Vec<i32>, k: i32) -> Vec<HashSet<i32>> {
let mut dp = Vec::new();
let mut prev = vec![HashSet::new(); k as usize + 1];
prev[0].insert(0);
for i in 0..nums.len() {
for j in (0..= min(k as usize - 1, i + 1)).rev() {
let (before, after) = prev.split_at_mut(j + 1);
for &x in &before[j] {
after[0].insert(x | nums[i]);
}
}
dp.push(prev[k as usize].clone());
}
dp
}
let a = find_ors(&nums, k);
let reversed_nums: Vec<i32> = nums.iter().rev().cloned().collect();
let b = find_ors(&reversed_nums, k);
let mut mx = 0;
for i in k as usize - 1..nums.len() - k as usize {
for &a_val in &a[i] {
for &b_val in &b[nums.len() - i - 2] {
mx = mx.max(a_val ^ b_val);
}
}
}
mx
}
}
复杂度分析
-
时间复杂度:$O(n \times k \times U + n \times U^2)$,其中 $n$ 是数组 $\textit{nums}$ 的长度,$U$ 是数组元素的可能性集合大小。$\textit{dp}$ 的状态有 $O(n\times k)$ 种,每个状态消耗 $O(U)$ 计算。最后两两求异或消耗 $O(n\times U^2)$ 的时间。
-
空间复杂度:$O(n \times U)$。
HarmonyOSNext 端云一体化(4)
HarmonyOSNext 端云一体化(4)
在上一章节我们讲了数据库数据表的一些基本操作。如query、upsert、delete和calculateQuery。这一章节主要来讲解各种查询条件操作。如 查询班级年龄大于30的同学等。
查询条件解释
谓词,用来代替或者展示其客体性质、特征或者客体之间关系的词项。
这些查询条件在端云一体中解释中叫做谓词。云数据库中提供丰富的谓词查询来构建查询条件。根据谓词查询方法构造自己的
DatabaseQuery对象。
查询条件谓词一览
关键字 | 说明 |
---|---|
equalTo | 表示等于的条件判断,用于查询中筛选出与指定值相等的数据 |
notEqualTo | 表示不等于的条件判断,筛选出与指定值不相等的数据 |
beginsWith | 表示以某个值开头,用于查询开头匹配特定字符串的数据 |
endsWith | 表示以某个值结尾,用于查询结尾匹配特定字符串的数据 |
contains | 表示包含某个值,用于查询包含特定字符串的数据 |
greaterThan | 表示大于,用于数值类型数据的比较,筛选出大于指定值的数据 |
greaterThanOrEqualTo | 表示大于或等于,筛选出大于或等于指定值的数据 |
lessThan | 表示小于,用于数值类型数据的比较,筛选出小于指定值的数据 |
lessThanOrEqualTo | 表示小于或等于,筛选出小于或等于指定值的数据 |
in | 用于判断某个值是否在指定的集合内,常用于查询符合多个值中某一个的数据 |
isNull | 用于判断某个字段是否为空值 |
isNotNull | 用于判断某个字段是否不为空值 |
orderByAsc | 按升序排列,用于对查询结果按照指定字段进行从小到大的排序 |
orderByDesc | 按降序排列,用于对查询结果按照指定字段进行从大到小的排序 |
limit | 限制查询结果返回的数量 |
beginGroup | 开始一个逻辑分组,用于将多个条件组合在一起作为一个逻辑单元 |
endGroup | 结束一个逻辑分组 |
or | 逻辑或,用于连接多个条件,只要其中一个条件满足则整个逻辑表达式为真 |
and | 逻辑与,用于连接多个条件,只有所有条件都满足时整个逻辑表达式才为真 |
谓词使用示例
equalTo 查询id为20的数据
this.condition.equalTo("id", 20)
notEqualTo 查询id不等于20的数据
this.condition.notEqualTo("id", 20)
beginsWith 查询name字段以b开头的数据
this.condition.beginsWith("name", "b")
endsWith 查询name字段以k结尾的数据
this.condition.endsWith("name", "k")
contains 查询name字段包含k的数据
this.condition.contains("name", "k")
greaterThan 查询price字段大于30的数据
this.condition.greaterThan("price", 30)
greaterThanOrEqualTo 查询price字段大于或者等于30的数据
this.condition.greaterThanOrEqualTo("price", 30)
lessThan 查询price字段小于30的数据
this.condition.lessThan("price", 30)
lessThanOrEqualTo 查询price字段小于或者等于30的数据
this.condition.lessThanOrEqualTo("price", 30)
in 查询name字段包含在["book","aaaa","bbbb"]的中数据
this.condition.in("name", ["book", "aaaa", "bbbb"])
isNull 查询name字段是否为null
this.condition.isNull("name")
isNotNull 查询name字段是否非null
this.condition.isNotNull("name")
orderByAsc 根据id,进行升序
this.condition.orderByAsc("id")
orderByDesc 根据id,进行降序
this.condition.orderByDesc("id")
limit 查询2条数据,从第1条开始
this.condition.limit(2, 1)
or 逻辑或,查询name=book 或者 price>30的数据
this.condition.equalTo("name", "book").or().greaterThan('price', 30)
and 逻辑与,查询name=book123 并且 price>30的数据
this.condition.equalTo("name", "book123").and().greaterThan('price', 30)
beginGroup 和 endGroup 表示一对逻辑分组
// 条件1: name=book并且price>30
// 条件2: id=20或者price>30
// 需求: 查询 条件1 和 条件2 同时满足的数据
this.condition
.beginGroup()
.equalTo('name', 30)
.and()
.greaterThan('price', 30)
.endGroup()
.and()
.beginGroup()
.equalTo('id', 20)
.or()
.greaterThan('price', 30)
.endGroup();
加强
上面的谓词,也是根据实际语义搭配一起使用。比如:查询name=book的前2条数据
总结
本章主要介绍了HarmonyOSNext端云一体化中的数据库查询条件操作:
-
介绍了查询条件中的谓词概念,它用于展示或描述数据的性质、特征或关系
-
详细列举了常用的查询谓词,包括:
- 比较类:equalTo、notEqualTo、greaterThan、lessThan等
- 字符串匹配:beginsWith、endsWith、contains
- 空值判断:isNull、isNotNull
- 集合操作:in
- 排序限制:orderByAsc、orderByDesc、limit
- 逻辑组合:and、or、beginGroup、endGroup
-
通过具体示例展示了各种谓词的使用方法,包括基本查询和复杂的组合查询
-
说明了谓词可以根据实际需求灵活组合使用,以实现更复杂的查询功能
如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯,欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。
每日一题-求出数组中最大序列值🔴
给你一个整数数组 nums
和一个 正 整数 k
。
定义长度为 2 * x
的序列 seq
的 值 为:
-
(seq[0] OR seq[1] OR ... OR seq[x - 1]) XOR (seq[x] OR seq[x + 1] OR ... OR seq[2 * x - 1])
.
请你求出 nums
中所有长度为 2 * k
的 子序列 的 最大值 。
示例 1:
输入:nums = [2,6,7], k = 1
输出:5
解释:
子序列 [2, 7]
的值最大,为 2 XOR 7 = 5
。
示例 2:
输入:nums = [4,2,5,6,7], k = 2
输出:2
解释:
子序列 [4, 5, 6, 7]
的值最大,为 (4 OR 5) XOR (6 OR 7) = 2
。
提示:
2 <= nums.length <= 400
1 <= nums[i] < 27
1 <= k <= nums.length / 2
jsonp解决前端跨域问题
各位大佬我们今天聊聊前端热门八股-跨域问题
那什么叫从一个域到另一个域呢?
在 Web 开发中,"域"(或"源")是指一个特定的协议、域名和端口的组合。浏览器的同源策略限制了从一个源加载的文档或脚本与另一个源的资源进行交互。理解域的概念对于理解跨域资源共享(CORS)非常重要。
域的组成
一个域由以下三个部分组成:
-
协议:如 http 或 https。
-
域名:如 example.com。
-
端口:如 80(HTTP 的默认端口)或 443(HTTPS 的默认端口)。
比如掘金的域
同源策略
同源策略是浏览器的一种安全机制,限制了从一个源加载的文档或脚本与另一个源的资源进行交互。只有当协议、域名和端口都相同时,两个 URL 才被认为是同源的。
但是为什么浏览器要阻止这种访问资源的行为,我们来说几个常见的跨域安全问题
1. 防止跨站请求伪造(CSRF)
- 跨站请求伪造是一种攻击方式,攻击者诱导用户的浏览器在用户不知情的情况下执行不当的操作。例如,用户登录到银行网站后,攻击者可能会诱导用户访问一个恶意网站,该网站会在用户不知情的情况下向银行网站发送请求,执行转账等操作。
2. 防止跨站脚本攻击(XSS)
- 跨站脚本攻击允许攻击者在其他网站的上下文中执行恶意脚本。通过限制跨域请求,浏览器可以减少攻击者在用户访问的其他网站上执行恶意脚本的机会。
3. 保护用户隐私
- 浏览器的同源策略保护用户的敏感信息不被恶意网站访问。例如,用户的会话信息、登录状态和其他敏感数据通常存储在 cookies 中,限制跨域请求可以防止这些信息被不可信的来源访问。
4. 防止数据泄露
- 如果没有同源策略,恶意网站可以轻松地从其他网站获取数据,可能导致数据泄露。例如,攻击者可以从用户访问的其他网站窃取个人信息、交易记录等。
5. 确保数据完整性
- 同源策略确保数据的完整性,防止恶意网站在用户不知情的情况下修改或操纵数据。
跨域请求
当一个网页尝试从不同的源请求资源时,就会发生跨域请求。例如:
-
同源请求:
-
页面 URL: example.com/page
-
请求 URL: example.com/api/data
-
这两个 URL 是同源的,因为它们的协议、域名和端口都相同。
-
跨域请求:
-
页面 URL: example.com/page
-
请求 URL: api.example.com/data
-
这两个 URL 是跨域的,因为它们的域名不同
我们用fetch模拟一下跨域访问
首先初始化一个后端项目,并且运行在3000端口
//http 服务启动
// commonjs模块规范node早期 引入http模块
const http=require('http');
const server=http.createServer((req,res)=>{
//异步回调
//当请求来到服务器后,该函数会被执行 req请求对象被解析,res响应对象被创建 http结束
//发送响应体
res.end('hello world');
});
console.log('服务已启动,端口号:3000');
});
我们使用nodemon热更新让后端跑在3000端口,使用live'server启动前端在5500端口我们前端用fecth请求3000端口会发现报错了,浏览器报了一个跨域访问出错,所以即使在同一个局域网,端口号不同的情况下,这算一个跨域请求
我们今天用jsonp来实现这跨域资源访问
<ul id="list"></ul>
<script src=''http://localhost:3000''></script>
<script>
function callback(data) {
list.innerHTML = data.map(user => `<li>${user.id+user.name}</li>`).join('');
}
</script>
后端
//http 服务启动
// commonjs模块规范node早期 引入http模块
const http=require('http');
const user=[
{
id:1,
name:'张三'
},
{
id:2,
name:'李四'
}]
const server=http.createServer((req,res)=>{
//异步回调
//当请求来到服务器后,该函数会被执行 req请求对象被解析,res响应对象被创建 http结束
//发送响应体
res.end('callback('+JSON.stringify(user)+')');
});
server.listen(3000,()=>{
console.log('服务已启动,端口号:3000');
});
首先我们把script的源设置在与后端同一个端口,这里小编设置在3000端口,我们把后端返回的数据使用js的api挂载在ul标签上,这段代码的script标签从3000端口加载资源,看似浏览器能正常解析后端返回的js数据但是
各位大佬先别运行这段代码,我们先来想想这段代码的致命问题是什么,不错script标签阻塞
我们在浏览器启动前端会发现控制台报了一个callback没定义的问题,其实这段代码按顺序执行的时候,浏览器先解析了上面的script标签,从3000端口加载资源,但是此时的前端执行了返回的callback函数,<script src="http://localhost:3000"> 直接加载了这个脚本,而没有定义 callback 函数,导致浏览器无法正确执行返回的 JavaScript 代码。所以我们引出今天的跨域解决方案jsonp
我们把回调函数,与跨域的src封装在jsonp函数中这样只要改动函数中的src便能正确的处理后端返回的数据
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="list"></ul>
<script>
let list = document.getElementById('list');
// function callback(data) {
// list.innerHTML = data.map(user => `<li>${user.id+user.name}</li>`).join('');
// }
// fetch('http://127.0.0.1:3000').then(res => res.json())
// fetch('http://localhost:3000')
let jsonp=(url,callback)=>{
let oScript=document.createElement('script')
oScript.src=url
document.body.appendChild(oScript)
window.callback=callback
}
jsonp('http://localhost:3000',(user)=>{ list.innerHTML = user.map(user => `<li>${user.id+user.name}</li>`).join('');})
</script>
</body>
</html>
我们把后端callback函数挂载到window上
-
动态创建
-
浏览器允许从不同源加载脚本文件,因此可以利用
-
回调函数:
-
服务器返回的数据被包装在一个回调函数中。客户端在请求时指定回调函数的名称,服务器将数据作为参数传递给这个回调函数。
-
数据传输:
-
服务器返回的响应是一个 JavaScript 文件,其中包含对回调函数的调用,并将数据作为参数传递。
后端
//http 服务启动
// commonjs模块规范node早期 引入http模块
const http=require('http');
const user=[
{
id:1,
name:'张三'
},
{
id:2,
name:'李四'
}]
const server=http.createServer((req,res)=>{
//异步回调
//当请求来到服务器后,该函数会被执行 req请求对象被解析,res响应对象被创建 http结束
//发送响应体
res.end('callback('+JSON.stringify(user)+')');
});
server.listen(3000,()=>{
console.log('服务已启动,端口号:3000');
});
我们重新打开浏览器我们发现后端返回的数据正确的被浏览器解析到页面上,这样我们便使用jsonp实现了简单的跨域资源访问
Rnote:Star 8.6k,github上的宝藏项目,手绘与手写画图笔记,用它画图做笔记超丝滑,值得尝试!
嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法
Rnote是一款开源的基于矢量的绘图应用,专为学生、教师以及绘图板用户设计。它支持草图绘制、手写笔记以及对文档和图片进行注释。Rnote提供了PDF和图片的导入导出功能、无限画布以及适应不同屏幕尺寸的UI界面。
功能特点
Rnote以其强大的功能和灵活的用户体验著称,以下是它的一些核心特性:
- 自适应UI:专注于手写笔输入,提供流畅的绘图体验。
- 压力感应:支持不同配置的笔触样式,实现压力感应输入。
- 形状工具:创建多种不同的形状。
- 编辑工具:移动、旋转、缩放和修改现有内容。
- 文档布局:提供多种文档扩展布局选项。
- 背景定制:自定义背景颜色、图案和大小。
- 页面格式:自定义页面格式。
- 声音效果:可选的笔触声音。
- 快捷键配置:可重新配置的手写笔按钮快捷键。
- 工作区浏览器:集成的工作区浏览器,快速访问相关文件。
- 拖放与剪贴板支持:支持拖放和剪贴板功能。
- 图片导入:支持PDF、位图和SVG图片导入。
- 文档导出:文档可以导出为SVG、PDF和Xopp格式,文档页面和选择可以导出为SVG、PNG和JPEG格式。
-
文件格式:保存和加载文档使用原生
.rnote
文件格式。 - 多文档工作:使用标签在多个文档之间工作。
- 自动保存与打印:支持自动保存和打印功能。
官方网站
Rnote拥有一个项目网站:rnote.flxzt.net
安装指南
Rnote支持多种操作系统,以下是各个平台的安装方法:
Linux
在Flahub上下载官方的Flatpak版本:点击这里。
在Flahub上下载
MacOS
感谢@dehesselle,Rnote现在可以在MacOS上作为应用包使用。查看仓库,最新版本可以在这里下载:点击这里。
下载MacOS应用包
Windows
从最新发布中下载Windows安装程序:点击这里。
下载Windows安装程序
也可以使用Winget安装:
winget install flxzt.rnote
降级
由于文件格式的不稳定性,有时可能需要降级到特定版本。
列出Flahub上所有可用的旧版本:
flatpak remote-info --log flathub com.github.flxzt.rnote
选择所需版本的提交,并使用以下命令降级:
sudo flatpak update --commit=<commit-hash> com.github.flxzt.rnote
降级后,可以使用以下命令固定或取消固定Flahub版本:
$ flatpak mask com.github.flxzt.rnote
$ flatpak mask --remove com.github.flxzt.rnote
要再次更新到最新版本,取消固定并运行flatpak update
。
截图预览
以下是Rnote的一些截图,展示了应用的不同功能和界面:
常见问题和已知问题
在使用Rnote时,可能会遇到以下问题:
-
拖放功能不工作:确保Rnote有权访问你拖放文件的位置。可以在Flatseal(Flahub权限管理器)中授权。
-
当前文件位置奇怪:当标题栏中显示的目录类似于
/run/user/1000/../
时,Rnote可能没有权限访问该目录。同样,在Flatseal中授权可以解决这个问题。 -
手写笔按钮移动画布/功能不正常:确保已安装并加载了
xf86-input-wacom
(X11驱动)、libinput
(Wayland)和libwacom
。 -
使用手写笔悬停时,某些屏幕区域的其他输入事件被阻止:这可能是手掌拒绝功能,但如果不需要,可以检查是否有左右手系统设置,并确保设置正确。Rnote无法禁用此功能。(讨论见#329)
-
手写笔按钮快捷方式映射不符合预期:在某些设备上,一个手写笔按钮被映射到专用的“橡皮擦”模式。在快捷方式设置中的按钮可能会不一致(次要/上按钮实际上是主要/下按钮,或相反)。要更改映射到此“橡皮擦”模式的工具,请按照以下步骤操作:
- 将手写笔悬停在画布上,并按住被怀疑映射到“橡皮擦”模式的按钮
- 在按住按钮的同时切换到所需的笔样式
- 释放按钮时,它应该切换回之前的笔样式
- “橡皮擦”模式中的笔样式现在应该被记住
字体
Rnote内置了以下字体:
- Grape Nuts:Grape Nuts是一种简单的手写休闲字体。
- OpenDyslexic-Regular:OpenDyslexic是一种针对一些常见阅读障碍症状设计的字体。
- TT2020Base-Regular:TT2020是一款先进的开源超现实主义多语言打字机字体,适用于新的十年。
- Virgil:Virgil是Excalidraw使用的字体。
同类项目比较
在开源社区中,还有其他一些类似的项目,如Excalidraw、Pizzara等。以下是它们的一些特点:
- Excalidraw:一款简洁的在线绘图工具,支持手写笔输入,但功能相对单一。
- Pizzara:一款创新的绘图应用,具有高级形状处理和无限缩放功能,但与Rnote相比,可能在文档注释方面稍显不足。
- Inkscape:一个强大的矢量图形编辑器,适用于创建和编辑SVG文件。
- Krita:一个专为概念艺术家、纹理画家、漫画家和插画家设计的开源绘画软件。
- MyPaint:一个简约的绘画软件,专注于提供流畅的绘图体验。
总的来说,Rnote在功能丰富性、自定义选项和跨平台支持方面具有明显优势。
通过本文的介绍,相信你已经对Rnote有了更深入的了解。如果你是手写笔记和绘图的爱好者,不妨尝试一下这款开源应用,相信它会给你带来不一样的体验。
项目地址
https://github.com/flxzt/rnote
使用SwiftUI+MVVM+Combine构建一个简化版V2EX客户端
背景
SwiftUI 是苹果推出的一种全新框架,专为开发者打造简单、高效、直观的开发体验。相比传统的 UIKit 开发模式,SwiftUI 让界面构建变得更容易,代码更精简,尤其是在响应式编程方面表现出色。作为一名开发者,我在学习 SwiftUI 的过程中,发现市面上的教程大多只讲解零散的功能点,很难系统性地帮助我们掌握 SwiftUI 开发。
为了更深入地理解 SwiftUI,同时提升自己的开发能力,我决定从零开始开发一个完整的 App。希望通过这篇文章的讲解,能帮助你也轻松上手。
我们将基于 SwiftUI、Combine 和 MVVM 架构来构建项目。这不仅是学习 SwiftUI 的理想方式,也是构建现代 iOS 应用的好实践。更重要的是,这篇文章适合初学者,无需担心基础问题。
最终源码已托管在 GitHub 仓库,可以参考。
项目截图
项目概述
通过本文的开发实践,我们将构建一个简化版的 V2EX 社区客户端。
接口地址
UI地址
功能特点
- 使用 SwiftUI 构建完全原生的声明式 UI。
- 基于 Combine 处理响应式编程和异步数据流。
- MVVM 架构,清晰的分层设计和可测试代码。
- 自定义缓存机制,支持本地存储。
- 多语言支持,包括英语和简体中文。
- 深色模式和浅色模式的无缝集成。
项目结构
应用按照以下目录组织:
V2EXClient
├── Services # 网络请求等服务
├── Utilties # 工具类
├── Extension # 扩展类
├── Core
├── Home # 首页
├── Models # 数据模型
├── Views # SwiftUI 界面
├── ViewModels # 视图模型,负责业务逻辑
├── Detail # 详情页
├── Models # 数据模型
├── Views # SwiftUI 界面
├── ViewModels # 视图模型,负责业务逻辑
开发流程
这里以开发首页最热列表页为例,讲解如何在项目中使用MVVM+Combine方式开发
1.数据模型
根据接口地址返回的数据对象,创建对应的数据模型:
/**
URL:
https://www.v2ex.com/api/topics/hot.json
Response:
{
...
}
*/
// MARK: - TopicModel - 帖子
struct TopicModel: Identifiable, Codable {
let node: NodeModel
let member: MemberModel
let lastReplyBy: String
let lastTouched: Double
let title: String
let url: String
let created, lastModified: Double
let deleted, replies: Int
let id: Int
let content: String
let contentRendered: String
enum CodingKeys: String, CodingKey {
case node, member
case lastReplyBy = "last_reply_by"
case lastTouched = "last_touched"
case title, url, created, deleted, content
case contentRendered = "content_rendered"
case lastModified = "last_modified"
case replies, id
}
}
2.数据服务类
拿到接口地址后,我们需要发起请求,拿到数据并发送出去,这里使用Publisher
1.封装网络请求类
class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(let url):
return "[🔥] Bad response from URL: \(url)"
case .unknown:
return "[⚠️] Unknown error occured"
}
}
}
static func download(url: URL, token: String? = nil) -> AnyPublisher<Data, Error> {
var request = URLRequest(url: url)
if let token = token {
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return URLSession.shared.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .default))
.tryMap({ try self.handleURLResponse(output: $0, url: url) })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw NetworkingError.badURLResponse(url: url)
}
return output.data
}
static func handleCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}
}
2.创建DataService
class TopicDataService: ObservableObject {
@Published var topics: [TopicModel] = []
private var topicSubscribtion: AnyCancellable?
init() {
getTopics()
}
func getTopics() {
guard let url = URL(string: "https://www.v2ex.com/api/topics/hot.json") else {
return
}
topicSubscribtion = NetworkingManager.download(url: url)
.decode(type: [TopicModel].self, decoder: JSONDecoder())
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedTopics in
self?.topics = returnedTopics
self?.topicSubscribtion?.cancel()
})
}
}
3.ViewModel
class HomeViewModel: ObservableObject {
@Published var topics: [TopicModel] = []
private let dataService = TopicDataService()
private var cancelables = Set<AnyCancellable>()
init() {
addSubscribers()
}
private func addSubscribers() {
dataService.$topics
.sink { [weak self] returnedTopics in
self?.topics = returnedTopics
}
.store(in: &cancelables)
}
}
4.更新UI页面
struct HomeView: View {
@StateObject private var vm: HomeViewModel = HomeViewModel()
var body: some View {
List {
ForEach(vm.topics) { topic in
TopicRowView(topic: topic)
}
}
.navigationTitle(
Text("Topic")
)
.navigationBarTitleDisplayMode(.inline)
}
}
源码
完整代码已托管在 GitHub 仓库。如果你感兴趣,可以下载运行并根据自己的需求扩展功能。
DP前后缀 + 枚举 ( 无合并 step by step)
Problem: 3287. 求出数组中最大序列值
思路
- 计算所有 下标 $j$ 前 $k$ 个数可能 $or$ 出的结果,记为$prefix[k-1][j+1][x]$
因为$1 <= nums[j] < 2^7$,所以 $x < 2^7$ - 同理,计算所有 下标 $j$ 后 $k$ 个数可能 $or$ 出的结果,记为$suffix[k-1][j][x]$
- 枚举可能的分割位置 $i$ ,计算此位置前后的 $xor$ 值 = $prefix[i+1]$ ^ $suffix[i]$
Code
###Python3
class Solution:
def maxValue(self, nums: List[int], k: int) -> int:
n = len(nums)
mx = reduce(or_, nums)
prefix = [[[False] * (mx + 1) for i in range(n + 1)] for j in range(k + 1)]
ans = -inf
# 1. prefix or for k
for i in range(n):
prefix[0][i + 1] = prefix[0][i].copy()
prefix[0][i + 1][nums[i]] = True
for i in range(1, k):
for j in range(i, n - k + 1):
x = nums[j]
# unselect
prefix[i][j + 1] = prefix[i][j].copy()
# select
for h in range(0, mx + 1):
if prefix[i - 1][j][h]:
prefix[i][j + 1][h | x] = True
# print( f[i])
# 2. suffix or for k
suffix = [[[False] * (mx + 1) for i in range(n + 1)] for j in range(k + 1)]
for i in range(n - 1, -1, -1):
suffix[0][i] = suffix[0][i + 1].copy()
suffix[0][i][nums[i]] = True
for i in range(1, k):
for j in range(n - 1, k - 1, -1):
x = nums[j]
# unselect
suffix[i][j] = suffix[i][j + 1].copy()
# select
for h in range(0, mx + 1):
if suffix[i - 1][j + 1][h]:
suffix[i][j][h | x] = True
# 3. 枚举 所有 prefix xor suffix
ans = -inf
for i in range(k - 1, n - k + 1): # [0, i] [i+1, i+k]
pre = prefix[k - 1][i + 1]
for j in range(1, mx + 1):
if not pre[j]:
continue
post = suffix[k - 1][i + 1]
for h in range(1, mx + 1):
if post[h]:
ans = max(ans, j ^ h)
return ans
复杂度
- 时间复杂度: $O(nkU)$,$n$ 是 $nums$ 的长度,$U$ 是 $nums$ 所有元素的 $OR$,最多为 $2 ^ 7 -1$
- 空间复杂度: $O(nkU)$
前后缀分解 + 二维 0-1 背包 + 优化所选元素个数 + 试填法(Python/Java/C++/Go)
题意
从 $\textit{nums}$ 中选一个长为 $2k$ 的子序列,计算其前一半的 OR,后一半的 OR,这两个 OR 再计算 XOR。
问:计算出的 XOR 最大能是多少?
核心思路
- 想象有一根分割线,把 $\textit{nums}$ 分成左右两部分,左和右分别计算所有长为 $k$ 的子序列的 OR 都有哪些值。比如左边计算出的 OR 有 $2,3,5$,右边计算出的 OR 有 $1,3,6$,那么两两组合计算 XOR,其中最大值即为答案。
- 枚举分割线的位置,把 $\textit{nums}$ 分割成一个前缀和一个后缀,问题变成:从前缀/后缀中选一个长为 $k$ 的子序列,这个子序列 OR 的结果能否等于 $x$?
把 OR 理解成一个类似加法的东西,转换成二维 0-1 背包。如果你不了解 0-1 背包,或者不理解为什么下面代码 $j$ 要倒序枚举,请看【基础算法精讲 18】。
二维:指背包有两个约束,一个是所选元素的个数是 $k$,另一个是所选元素的 OR 是 $x$。
具体算法
计算后缀。对于 0-1 背包问题,我们定义 $f[i][j][x]$ 表示从 $\textit{nums}[i]$ 到 $\textit{nums}[n-1]$ 中选 $j$ 个数,这些数的 OR 能否等于 $x$。
设 $v=\textit{nums}[i]$,用刷表法转移:
- 不选 $v$,那么 $f[i][j][x] = f[i+1][j][x]$。
- 选 $v$,如果 $f[i+1][j][x]=\texttt{true}$,那么 $f[i][j+1][x\ |\ v]=\texttt{true}$。
刷表法:本题计算 $x = v\ |\ ?$ 中的 $?$ 是困难的,但计算 $x\ |\ v$ 是很简单的。也就是说,对于状态 $f[i][j][x]$ 而言,其转移来源是谁不好计算,但从 $f[i][j][x]$ 转移到的目标状态 $f[i][j+1][x\ |\ v]$ 是好计算的。在动态规划中,根据转移来源计算状态叫查表法,用当前状态更新其他状态叫刷表法。
初始值 $f[n][0][0]=\texttt{true}$。什么也不选,OR 等于 $0$。
对于每个 $i$,由于我们只需要 $f[i][k]$ 中的数据,把 $f[i][k]$ 复制到 $\textit{suf}[i]$ 中。这样做无需创建三维数组,空间复杂度更小。
代码实现时,$f$ 的第一个维度可以优化掉。
对于前缀 $\textit{pre}$ 的计算也同理。
最后,枚举 $i=k-1,k,k+1,\ldots,n-k-1$,两两组合 $\textit{pre}[i]$ 和 $\textit{suf}[i+1]$ 中的数计算 XOR,其中最大值即为答案。
小优化:如果在循环中,发现答案 $\textit{ans}$ 达到了理论最大值 $2^7-1$(或者所有元素的 OR),则立刻返回答案。
也可以用哈希集合代替布尔数组,见下面的 Python 优化代码。
具体请看 视频讲解 第三题,欢迎点赞关注~
优化前
###py
class Solution:
def maxValue(self, nums: List[int], k: int) -> int:
mx = reduce(or_, nums)
n = len(nums)
suf = [None] * (n - k + 1)
f = [[False] * (mx + 1) for _ in range(k + 1)]
f[0][0] = True
for i in range(n - 1, k - 1, -1):
v = nums[i]
# 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 False
for j in range(min(k - 1, n - 1 - i), -1, -1):
for x, has_x in enumerate(f[j]):
if has_x:
f[j + 1][x | v] = True
if i <= n - k:
suf[i] = f[k].copy()
ans = 0
pre = [[False] * (mx + 1) for _ in range(k + 1)]
pre[0][0] = True
for i, v in enumerate(nums[:-k]):
for j in range(min(k - 1, i), -1, -1):
for x, has_x in enumerate(pre[j]):
if has_x:
pre[j + 1][x | v] = True
if i < k - 1:
continue
for x, has_x in enumerate(pre[k]):
if has_x:
for y, has_y in enumerate(suf[i + 1]):
if has_y and x ^ y > ans: # 手写 if
ans = x ^ y
if ans == mx:
return ans
return ans
###py
# 使用 set 代替 bool list
class Solution:
def maxValue(self, nums: List[int], k: int) -> int:
n = len(nums)
suf = [None] * (n - k + 1)
f = [set() for _ in range(k + 1)]
f[0].add(0)
for i in range(n - 1, k - 1, -1):
v = nums[i]
for j in range(min(k - 1, n - 1 - i), -1, -1):
f[j + 1].update(x | v for x in f[j])
if i <= n - k:
suf[i] = f[k].copy()
mx = reduce(or_, nums)
ans = 0
pre = [set() for _ in range(k + 1)]
pre[0].add(0)
for i, v in enumerate(nums[:-k]):
for j in range(min(k - 1, i), -1, -1):
pre[j + 1].update(x | v for x in pre[j])
if i < k - 1:
continue
ans = max(ans, max(x ^ y for x in pre[k] for y in suf[i + 1]))
if ans == mx:
return ans
return ans
###java
class Solution {
public int maxValue(int[] nums, int k) {
final int MX = 1 << 7;
int n = nums.length;
boolean[][] suf = new boolean[n - k + 1][];
boolean[][] f = new boolean[k + 1][MX];
f[0][0] = true;
for (int i = n - 1; i >= k; i--) {
int v = nums[i];
// 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
for (int j = Math.min(k - 1, n - 1 - i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (f[j][x]) {
f[j + 1][x | v] = true;
}
}
}
if (i <= n - k) {
suf[i] = f[k].clone();
}
}
int ans = 0;
boolean[][] pre = new boolean[k + 1][MX];
pre[0][0] = true;
for (int i = 0; i < n - k; i++) {
int v = nums[i];
for (int j = Math.min(k - 1, i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (pre[j][x]) {
pre[j + 1][x | v] = true;
}
}
}
if (i < k - 1) {
continue;
}
for (int x = 0; x < MX; x++) {
if (pre[k][x]) {
for (int y = 0; y < MX; y++) {
if (suf[i + 1][y]) {
ans = Math.max(ans, x ^ y);
}
}
}
}
if (ans == MX - 1) {
return ans;
}
}
return ans;
}
}
###cpp
class Solution {
public:
int maxValue(vector<int>& nums, int k) {
const int MX = 1 << 7;
int n = nums.size();
vector<array<int, MX>> suf(n - k + 1);
vector<array<int, MX>> f(k + 1);
f[0][0] = true;
for (int i = n - 1; i >= k; i--) {
int v = nums[i];
// 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
for (int j = min(k - 1, n - 1 - i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (f[j][x]) {
f[j + 1][x | v] = true;
}
}
}
if (i <= n - k) {
suf[i] = f[k];
}
}
int ans = 0;
vector<array<int, MX>> pre(k + 1);
pre[0][0] = true;
for (int i = 0; i < n - k; i++) {
int v = nums[i];
for (int j = min(k - 1, i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (pre[j][x]) {
pre[j + 1][x | v] = true;
}
}
}
if (i < k - 1) {
continue;
}
for (int x = 0; x < MX; x++) {
if (pre[k][x]) {
for (int y = 0; y < MX; y++) {
if (suf[i + 1][y]) {
ans = max(ans, x ^ y);
}
}
}
}
if (ans == MX - 1) {
return ans;
}
}
return ans;
}
};
###go
func maxValue(nums []int, k int) (ans int) {
const mx = 1 << 7
n := len(nums)
suf := make([][mx]bool, n-k+1)
f := make([][mx]bool, k+1)
f[0][0] = true
for i := n - 1; i >= k; i-- {
v := nums[i]
// 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
for j := min(k-1, n-1-i); j >= 0; j-- {
for x, hasX := range f[j] {
if hasX {
f[j+1][x|v] = true
}
}
}
if i <= n-k {
suf[i] = f[k]
}
}
pre := make([][mx]bool, k+1)
pre[0][0] = true
for i, v := range nums[:n-k] {
for j := min(k-1, i); j >= 0; j-- {
for x, hasX := range pre[j] {
if hasX {
pre[j+1][x|v] = true
}
}
}
if i < k-1 {
continue
}
for x, hasX := range pre[k] {
if hasX {
for y, hasY := range suf[i+1] {
if hasY {
ans = max(ans, x^y)
}
}
}
}
if ans == mx-1 {
return
}
}
return
}
复杂度分析
- 时间复杂度:$\mathcal{O}(nkU + nU^2)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U$ 是 $\textit{nums}$ 所有元素的 OR,本题至多为 $2^7-1$。DP 是 $\mathcal{O}(nkU)$ 的,计算 XOR 最大值是 $\mathcal{O}(nU^2)$ 的。
- 空间复杂度:$\mathcal{O}(nU)$。
优化
例如 $x=1101_{(2)}$,我们至多要选几个 $\textit{nums}[i]$,就能 OR 得到 $x$?(前提是可以得到 $x$)
答案是 $3$ 个。考虑 $x$ 中的每个比特 $1$,它来自某个 $\textit{nums}[i]$。
设 $\textit{nums}$ 所有元素 OR 的二进制中的 $1$ 的个数为 $\textit{ones}$(本题数据范围保证 $textit{ones}\le 7$)。一般地,我们至多选 $\textit{ones}$ 个 $\textit{nums}[i]$,就能 OR 得到 $x$。
但是,本题要求(前缀/后缀)恰好选 $k$ 个元素。选的元素越多 OR 越大,那么某些比较小的 $x$ 可能无法 OR 出来。
为了判断(前缀/后缀)恰好选 $k$ 个元素能否 OR 出整数 $x$,定义:
- $\textit{minI}[x]$,表示从 $0$ 开始遍历,至少要遍历到 $i$ 才有可能找到 $k$ 个数 OR 等于 $x$。如果无法得到 $x$ 那么 $\textit{minI}[x] = \infty$。
- $\textit{maxI}[x]$,表示从 $n-1$ 开始遍历,至少要遍历到 $i$ 才有可能找到 $k$ 个数 OR 等于 $x$。如果无法得到 $x$ 那么 $\textit{maxI}[x] = 0$。
根据 从集合论到位运算,如果能 OR 得到 $x$,那么参与 OR 运算的元素都是 $x$ 的子集。换句话说,$x$ 是参与 OR 运算的元素的超集(superset)。
对于 $\textit{minI}[x]$ 的计算,我们可以在遍历 $\textit{nums}$ 的同时,用一个数组 $\textit{cnt}$ 维护 $\textit{nums}$ 元素的超集的出现次数。如果发现 $\textit{cnt}[x]=k$,说明至少要遍历到 $i$ 才有可能找到 $k$ 个数 OR 等于 $x$,记录 $\textit{minI}[x]=i$。对于 $\textit{maxI}[x]$ 的计算也同理。
对于两数异或最大值的计算,可以用试填法解决,原理请看【图解】421. 数组中两个数的最大异或值。
###py
class Solution:
def maxValue(self, nums: List[int], k: int) -> int:
n = len(nums)
mx = reduce(or_, nums)
k2 = min(k, mx.bit_count()) # 至多选 k2 个数
suf = [None] * (n - k + 1)
f = [set() for _ in range(k2 + 1)]
f[0].add(0)
max_i = [0] * (mx + 1)
cnt = [0] * (mx + 1)
for i in range(n - 1, k - 1, -1):
v = nums[i]
for j in range(min(k2 - 1, n - 1 - i), -1, -1):
f[j + 1].update(x | v for x in f[j])
if i <= n - k:
suf[i] = f[k2].copy()
# 枚举 v 的超集
s = v
while s <= mx:
cnt[s] += 1
if cnt[s] == k:
# 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
max_i[s] = i
s = (s + 1) | v
ans = 0
pre = [set() for _ in range(k2 + 1)]
pre[0].add(0)
min_i = [inf] * (mx + 1)
cnt = [0] * (mx + 1)
w = mx.bit_length() # 用于 findMaximumXOR
for i, v in enumerate(nums[:-k]):
for j in range(min(k2 - 1, i), -1, -1):
pre[j + 1].update(x | v for x in pre[j])
# 枚举 v 的超集
s = v
while s <= mx:
cnt[s] += 1
if cnt[s] == k:
# 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
min_i[s] = i
s = (s + 1) | v
if i < k - 1:
continue
a = [x for x in pre[k2] if min_i[x] <= i]
b = [x for x in suf[i + 1] if max_i[x] > i]
ans = max(ans, self.findMaximumXOR(a, b, w))
if ans == mx:
return ans
return ans
# 421. 数组中两个数的最大异或值
# 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
def findMaximumXOR(self, a: List[int], b: List[int], w: int) -> int:
ans = mask = 0
for i in range(w - 1, -1, -1): # 从最高位开始枚举
mask |= 1 << i
new_ans = ans | (1 << i) # 这个比特位可以是 1 吗?
set_a = set(x & mask for x in a) # 低于 i 的比特位置为 0
for x in b:
x &= mask # 低于 i 的比特位置为 0
if new_ans ^ x in set_a:
ans = new_ans # 这个比特位可以是 1
break
return ans
###java
class Solution {
private static final int BIT_WIDTH = 7;
public int maxValue(int[] nums, int k) {
final int MX = 1 << BIT_WIDTH;
int n = nums.length;
int k2 = Math.min(k, BIT_WIDTH); // 至多选 k2 个数
boolean[][] suf = new boolean[n - k + 1][];
boolean[][] f = new boolean[k2 + 1][MX];
f[0][0] = true;
int[] maxI = new int[MX];
int[] cnt = new int[MX];
for (int i = n - 1; i >= k; i--) {
int v = nums[i];
for (int j = Math.min(k2 - 1, n - 1 - i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (f[j][x]) {
f[j + 1][x | v] = true;
}
}
}
if (i <= n - k) {
suf[i] = f[k2].clone();
}
// 枚举 v 的超集
for (int s = v; s < MX; s = (s + 1) | v) {
if (++cnt[s] == k) {
// 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
maxI[s] = i;
}
}
}
int ans = 0;
boolean[][] pre = new boolean[k2 + 1][MX];
pre[0][0] = true;
int[] minI = new int[MX];
Arrays.fill(minI, Integer.MAX_VALUE);
Arrays.fill(cnt, 0);
int[] a = new int[MX];
int[] b = new int[MX];
for (int i = 0; i < n - k; i++) {
int v = nums[i];
for (int j = Math.min(k2 - 1, i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (pre[j][x]) {
pre[j + 1][x | v] = true;
}
}
}
// 枚举 v 的超集
for (int s = v; s < MX; s = (s + 1) | v) {
if (++cnt[s] == k) {
// 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
minI[s] = i;
}
}
if (i < k - 1) {
continue;
}
int na = 0;
int nb = 0;
for (int x = 0; x < MX; x++) {
if (pre[k2][x] && minI[x] <= i) {
a[na++] = x;
}
if (suf[i + 1][x] && maxI[x] > i) {
b[nb++] = x;
}
}
ans = Math.max(ans, findMaximumXOR(a, na, b, nb));
if (ans == MX - 1) {
return ans;
}
}
return ans;
}
// 421. 数组中两个数的最大异或值
// 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
private int findMaximumXOR(int[] a, int n, int[] b, int m) {
int ans = 0;
int mask = 0;
boolean[] seen = new boolean[1 << BIT_WIDTH];
for (int i = BIT_WIDTH - 1; i >= 0; i--) { // 从最高位开始枚举
mask |= 1 << i;
int newAns = ans | (1 << i); // 这个比特位可以是 1 吗?
Arrays.fill(seen, false);
for (int j = 0; j < n; j++) {
seen[a[j] & mask] = true; // 低于 i 的比特位置为 0
}
for (int j = 0; j < m; j++) {
int x = b[j] & mask; // 低于 i 的比特位置为 0
if (seen[newAns ^ x]) {
ans = newAns; // 这个比特位可以是 1
break;
}
}
}
return ans;
}
}
###cpp
class Solution {
static constexpr int BIT_WIDTH = 7;
// 421. 数组中两个数的最大异或值
// 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
int findMaximumXOR(vector<int>& a, vector<int>& b) {
int ans = 0, mask = 0;
vector<int> seen(1 << BIT_WIDTH);
for (int i = BIT_WIDTH - 1; i >= 0; i--) { // 从最高位开始枚举
mask |= 1 << i;
int new_ans = ans | (1 << i); // 这个比特位可以是 1 吗?
ranges::fill(seen, false);
for (int x : a) {
seen[x & mask] = true; // 低于 i 的比特位置为 0
}
for (int x : b) {
x &= mask; // 低于 i 的比特位置为 0
if (seen[new_ans ^ x]) {
ans = new_ans; // 这个比特位可以是 1
break;
}
}
}
return ans;
}
public:
int maxValue(vector<int>& nums, int k) {
const int MX = 1 << BIT_WIDTH;
int n = nums.size();
int k2 = min(k, BIT_WIDTH); // 至多选 k2 个数
vector<array<int, MX>> suf(n - k + 1);
vector<array<int, MX>> f(k2 + 1);
f[0][0] = true;
int max_i[MX]{}, cnt[MX]{};
for (int i = n - 1; i >= k; i--) {
int v = nums[i];
// 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
for (int j = min(k2 - 1, n - 1 - i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (f[j][x]) {
f[j + 1][x | v] = true;
}
}
}
if (i <= n - k) {
suf[i] = f[k2];
}
// 枚举 v 的超集
for (int s = v; s < MX; s = (s + 1) | v) {
if (++cnt[s] == k) {
// 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
max_i[s] = i;
}
}
}
int ans = 0;
vector<array<int, MX>> pre(k2 + 1);
pre[0][0] = true;
int min_i[MX];
ranges::fill(min_i, INT_MAX);
ranges::fill(cnt, 0);
for (int i = 0; i < n - k; i++) {
int v = nums[i];
for (int j = min(k2 - 1, i); j >= 0; j--) {
for (int x = 0; x < MX; x++) {
if (pre[j][x]) {
pre[j + 1][x | v] = true;
}
}
}
// 枚举 v 的超集
for (int s = v; s < MX; s = (s + 1) | v) {
if (++cnt[s] == k) {
// 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
min_i[s] = i;
}
}
if (i < k - 1) {
continue;
}
vector<int> a, b;
for (int x = 0; x < MX; x++) {
if (pre[k2][x] && min_i[x] <= i) {
a.push_back(x);
}
if (suf[i + 1][x] && max_i[x] > i) {
b.push_back(x);
}
}
ans = max(ans, findMaximumXOR(a, b));
if (ans == MX - 1) {
return ans;
}
}
return ans;
}
};
###go
const bitWidth = 7
const mx = 1 << bitWidth
func maxValue(nums []int, k int) (ans int) {
n := len(nums)
k2 := min(k, bitWidth) // 至多选 k2 个数
suf := make([][mx]bool, n-k+1)
f := make([][mx]bool, k2+1)
f[0][0] = true
maxI := [mx]int{}
cnt := [mx]int{}
for i := n - 1; i >= k; i-- {
v := nums[i]
for j := min(k2-1, n-1-i); j >= 0; j-- {
for x, hasX := range f[j] {
if hasX {
f[j+1][x|v] = true
}
}
}
if i <= n-k {
suf[i] = f[k2]
}
// 枚举 v 的超集
for s := v; s < mx; s = (s + 1) | v {
cnt[s]++
if cnt[s] == k {
// 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
maxI[s] = i
}
}
}
pre := make([][mx]bool, k2+1)
pre[0][0] = true
minI := [mx]int{}
for i := range minI {
minI[i] = math.MaxInt
}
cnt = [mx]int{}
for i, v := range nums[:n-k] {
for j := min(k2-1, i); j >= 0; j-- {
for x, hasX := range pre[j] {
if hasX {
pre[j+1][x|v] = true
}
}
}
// 枚举 v 的超集
for s := v; s < mx; s = (s + 1) | v {
cnt[s]++
if cnt[s] == k {
// 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
minI[s] = i
}
}
if i < k-1 {
continue
}
a := []int{}
b := []int{}
for x, has := range pre[k2] {
if has && minI[x] <= i {
a = append(a, x)
}
if suf[i+1][x] && maxI[x] > i {
b = append(b, x)
}
}
ans = max(ans, findMaximumXOR(a, b))
if ans == mx-1 {
return
}
}
return
}
// 421. 数组中两个数的最大异或值
// 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
func findMaximumXOR(a, b []int) (ans int) {
mask := 0
for i := bitWidth - 1; i >= 0; i-- { // 从最高位开始枚举
mask |= 1 << i
newAns := ans | 1<<i // 这个比特位可以是 1 吗?
seen := [mx]bool{}
for _, x := range a {
seen[x&mask] = true // 低于 i 的比特位置为 0
}
for _, x := range b {
x &= mask // 低于 i 的比特位置为 0
if seen[newAns^x] {
ans = newAns
break
}
}
}
return
}
复杂度分析
- 时间复杂度:$\mathcal{O}(nU\log U)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U$ 是 $\textit{nums}$ 所有元素的 OR,本题至多为 $2^7-1$。
- 空间复杂度:$\mathcal{O}(nU)$。
更多相似题目,见下面动态规划题单中的「§3.1 0-1 背包」和「专题:前后缀分解」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
枚举 & DP
解法:枚举 & DP
一般这种一个序列分两半的题目,我们会尝试枚举分界点。
设 $f(i, j, x)$ 表示从前 $i$ 个元素中选出 $j$ 个,OR 起来能否得到 $x$;$g(i, j, x)$ 表示从第 $i$ 个元素到最后一个元素中选出 $j$ 个,OR 起来能否得到 $x$。我们枚举子序列左半边的结束点 $i$,左半边 OR 起来的值 $x$,以及右半边 OR 起来的值 $y$,那么答案就是
$$
\max\limits_{f(i, k, x) = \text{true and } g(i + 1, k, y) = \text{true}} x \oplus y
$$
枚举的复杂度是 $\mathcal{O}(n \times m^2)$,其中 $m = 2^7$ 是题目中提到的取值范围。
剩下的问题就是 $f$ 和 $g$ 怎么求。考虑是否选择第 $i$ 个元素,可以得到转移方程
f(i, j, x) -> f(i + 1, j, x) // 不选 nums[i]
f(i, j, x) -> f(i + 1, j + 1, x | nums[i]) // 选 nums[i]
初值 f(0, 0, 0) = true
,其它位置初值为 false
。$g$ 的求法类似。这一部分的复杂度为 $\mathcal{O}(nkm)$。
参考代码(c++)
###cpp
bool f[410][210][1 << 7], g[410][210][1 << 7];
class Solution {
public:
int maxValue(vector<int>& nums, int K) {
int n = nums.size();
int m = 1 << 7;
for (int i = 0; i <= n + 1; i++) for (int j = 0; j <= K; j++) for (int x = 0; x < m; x++)
f[i][j][x] = g[i][j][x] = false;
auto update = [&](bool &x, bool y) { x = x || y; };
// DP 求子序列前半部分的情况
f[0][0][0] = true;
for (int i = 0; i < n; i++) for (int j = 0; j <= K; j++) for (int x = 0; x < m; x++) if (f[i][j][x]) {
update(f[i + 1][j][x], f[i][j][x]);
if (j < K) update(f[i + 1][j + 1][x | nums[i]], f[i][j][x]);
}
// DP 求子序列后半部分的情况
g[n + 1][0][0] = true;
for (int i = n + 1; i > 1; i--) for (int j = 0; j <= K; j++) for (int x = 0; x < m; x++) if (g[i][j][x]) {
update(g[i - 1][j][x], g[i][j][x]);
if (j < K) update(g[i - 1][j + 1][x | nums[i - 2]], g[i][j][x]);
}
int ans = 0;
// 枚举子序列的分界点,以及前后半的值
for (int i = K; i + K <= n; i++) for (int x = 0; x < m; x++) for (int y = 0; y < m; y++)
if (f[i][K][x] && g[i + 1][K][y]) ans = max(ans, x ^ y);
return ans;
}
};
美最高法支持政府对TikTok强迫出售令
Langchian.js |Embedding & Vector Store👈| 数据向量化后这样储存😱
前言
书接上文 , 学习了分割多个文档对象 , 这一次要学习
- 如何将数据向量化 ? 😍
- 向量化的数据持久化储存 ? 😍
也就是说 ,下面这张图 ,要 over 了 , 🤡👈
Embedding
langchain.js 在文本处理领域 ,不仅提供我前面所学的文本分割与转换 , 也为文本的向量化提供了支持 , 这不禁让应用开发者尖叫 ~ , 所谓文本的嵌入 , 其机制就是 : 将复杂文本数据转换为具有固定维度的向量 , 在机器学习和检索任务中十分 nice ~
Embedding 就是嵌入 , 他是 Langchain.js 的一个核心组件
主要作用是 , 为各种文本嵌入模型交互而设计 , 为许多的模型提供统一的 、标准化的接口 ; 说到这里 , 我们可以思考 : 其实 langchain 框架本身就是为了提供“统一化 、标准化的接口”而生 , 它是 LLM 的上层应用框架 , 成为开发层面的老大 , 底层调用各类模型 , 我们开发者只需要熟悉固定的语法 , 痛苦都交给了 langchain 🤡
langchain 支持的嵌入式模型如下 :
Name | Description |
---|---|
Alibaba Tongyi | The AlibabaTongyiEmbeddings class uses the Alibaba Tongyi API to gene... |
Azure OpenAI | [Azure |
Baidu Qianfan | The BaiduQianfanEmbeddings class uses the Baidu Qianfan API to genera... |
Amazon Bedrock | Amazon Bedrock is a fully managed |
ByteDance Doubao | This will help you get started with ByteDanceDoubao [embedding |
Cloudflare Workers AI | This will help you get started with Cloudflare Workers AI [embedding |
Cohere | This will help you get started with CohereEmbeddings [embedding |
DeepInfra | The DeepInfraEmbeddings class utilizes the DeepInfra API to generate ... |
Fireworks | This will help you get started with FireworksEmbeddings [embedding |
Google Generative AI | This will help you get started with Google Generative AI [embedding |
Google Vertex AI | Google Vertex is a service that |
Gradient AI | The GradientEmbeddings class uses the Gradient AI API to generate emb... |
HuggingFace Inference | This Embeddings integration uses the HuggingFace Inference API to gen... |
IBM watsonx.ai | This will help you get started with IBM watsonx.ai [embedding |
Jina | The JinaEmbeddings class utilizes the Jina API to generate embeddings... |
Llama CPP | Only available on Node.js. |
Minimax | The MinimaxEmbeddings class uses the Minimax API to generate embeddin... |
MistralAI | This will help you get started with MistralAIEmbeddings [embedding |
Mixedbread AI | The MixedbreadAIEmbeddings class uses the Mixedbread AI API to genera... |
Nomic | The NomicEmbeddings class uses the Nomic AI API to generate embedding... |
Ollama | This will help you get started with Ollama [embedding |
OpenAI | This will help you get started with OpenAIEmbeddings [embedding |
Pinecone | This will help you get started with PineconeEmbeddings [embedding |
Prem AI | The PremEmbeddings class uses the Prem AI API to generate embeddings ... |
Tencent Hunyuan | The TencentHunyuanEmbeddings class uses the Tencent Hunyuan API to ge... |
TensorFlow | This Embeddings integration runs the embeddings entirely in your brow... |
TogetherAI | This will help you get started with TogetherAIEmbeddings [embedding |
HuggingFace Transformers | The TransformerEmbeddings class uses the Transformers.js package to g... |
Voyage AI | The VoyageEmbeddings class uses the Voyage AI REST API to generate em... |
ZhipuAI | The ZhipuAIEmbeddings class uses the ZhipuAI API to generate embeddin... |
参考自官网 :js.langchain.com/docs/integr…
这些模型支持嵌入式 , 即支持将文本向量化 ~
我将使用 openai 来演示 ,
- 首先加载 data 文件夹下的"少年中国说.txt"文件为 Document 对象
- 然后使用工具分割对象
- 使用嵌入式模型向量化第二步分割后的 chunk
import { load } from "dotenv";
import { OpenAIEmbeddings } from "@langchain/openai";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const env = await load();
const process = {
env
}
// 1.
const loader = new TextLoader("data/少年中国说.txt");
const docs = await loader.load();
// 2.
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 100, // 切片大小
chunkOverlap: 20,// 重叠部分
});
const splitDocs = await splitter.splitDocuments(docs);
// 3.
const embeddings = new OpenAIEmbeddings()
const splitDoc = splitDocs[0].pageContent
const res = await embeddings.embedQuery(splitDoc)
console.log(res)
在向量化之前 , 打印splitDocs , 输出如下:
使用OpenAIEmbeddings 嵌入式模型 , 对上述输出向量化之后 , 变成如下 :
上述代码使用嵌入式模型将文本变成了向量
会经历一下过程 :
-
预处理
分词:首先,文本需要被分割成更小的单元,如单词或子词(subword)。例如,中文文本通常会被切分成单个汉字或词语。
标准化:去除标点符号、转换为小写等操作,以确保一致性。 -
词汇表构建
模型会根据训练数据构建一个词汇表(vocabulary),其中每个词都对应一个唯一的索引。对于未出现在词汇表中的词,通常会有一个特殊的标记(如)来表示未知词。 -
词向量生成
静态词向量:早期的方法如Word2Vec、GloVe等会为每个词生成一个固定长度的向量。这些向量是通过无监督学习从大量文本中训练得到的,能够捕捉到词与词之间的语义关系。
动态词向量:现代模型如BERT、OpenAI的模型使用的是上下文敏感的词向量。这意味着同一个词在不同的句子中可能会有不同的向量表示,从而更好地捕捉其在特定上下文中的含义。 -
句子编码
平均池化:一种简单的方法是将句子中所有词向量的平均值作为句子的向量表示。
加权求和:可以对词向量进行加权求和,权重可以根据词的重要性(如TF-IDF)来确定。
Transformer架构:现代模型如BERT、OpenAI的模型使用了自注意力机制(self-attention),能够更好地捕捉句子中的长距离依赖关系,并生成整个句子的向量表示。 -
嵌入层
在神经网络中,嵌入层(Embedding Layer)负责将输入的词索引转换为对应的词向量。这个层通常是可训练的,可以在下游任务中进一步优化。 -
输出向量
最终,模型会输出一个固定长度的向量,这个向量代表了输入文本片段的语义信息。这个向量可以用于各种自然语言处理任务,如相似度计算、分类等。
以上过程参考自网络
Vector Store
向量数据库主要由 LangChain 社区维护的第三方集成 , 即在@langchain/community
包下面
关于选取那个数据 ,请查阅:js.langchain.ac.cn/docs/integr…
下面介绍两种向量数据库
- Chroma
- FaissStore
Chroma
一个专门为嵌入式向量设计的基于 SQLite 的开源数据库 , 有如下特点
- 容易用
- 轻量
- 智能
通过向量切分多个段落 , 并对每个段落独立进行 k-means 聚类 , Chroma 可以有效压缩数据 , 减少储存空间 , 提高查询效率
k-means 聚类是一种无监督学习算法。它将数据分为 k 个聚类,使得每个数据点都属于离它最近的聚类中心所属的聚类。 通过不断迭代更新聚类中心,直到达到某种收敛条件。 例如,在图像识别中,可以用 k-means 聚类对图像的颜色进行分类;在客户细分中, 可以根据客户的特征将客户分为不同的群体。
langchain.js 官网 : js.langchain.ac.cn/docs/integr…
Chroma 官网 : docs.trychroma.com/docs/overvi…
好家伙 , 只支持 python 和 ts 🤡
安装、使用 ,依照上面官网
FaissStore
Faiss 是一个用于高效相似性搜索和密集向量聚类的库。
LangChain.js 支持使用 Faiss 作为本地运行的向量存储,可以保存到文件。
它还提供从 LangChain Python 实现 读取保存的文件的能力。
我在官网上看到这段 , 从那一眼起 , 我就选择她了 , 可是让我无语的是 , 我熬夜到天亮改了一个很臭的 bug —— 使用 npm , yarn , pnpm ... , 从淘宝源到腾讯源 , 这个包总是下不下来 , 我就不断搜索 , 可惜我用的是 Edge , 全是 csdn , 直到我在 github 上搜到以下解决方案 ,非常 nice !
一言蔽之即 : 手动下载 realse 版本 , 将无法下载的文件 ,手动添加到 node_modules
愿以我之发 , 保倔友之发🤡
总结 : 不要使用诸如 Edge 之类的浏览器搜报错🤡👈
实战
package.json
{
"name": "test-app-node",
"private": true,
"version": "0.0.0",
"scripts": {
"prepare-kong-faiss": "ts-node prepare-kong-faiss.ts",
"load-kong-faiss": "ts-node load-kong-faiss.ts",
"multiQueryRetriever": "ts-node multiQueryRetriever.ts",
"LLMChainExtractor": "ts-node LLMChainExtractor.ts",
"ScoreThresholdRetriever": "ts-node ScoreThresholdRetriever.ts",
"prepare-qiu": "ts-node ./rag/prepare-qiu.ts",
"rag-2": "ts-node ./rag/index.ts",
"rag-server": "ts-node ./rag/server.ts",
"rag-client": "ts-node ./rag/client.ts"
},
"type": "module",
"dependencies": {
"@langchain/community": "^0.0.27",
"dotenv": "^16.4.7",
"express": "^4.19.2",
"faiss-node": "^0.5.1",
"langchain": "^0.1.37",
"typescript": "^5.7.3"
},
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"ts-node": "^10.9.2"
}
}
embedding
安装好上述包后 , 使用嵌入式模型 将向量化后的数据储存在 data/vector/ 下
import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { FaissStore } from "langchain/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import "dotenv/config";
const run = async () => {
const loader = new TextLoader("../data/少年中国说.txt");
const docs = await loader.load();
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 100,
chunkOverlap: 20,
});
const splitDocs = await splitter.splitDocuments(docs);
const embeddings = new OpenAIEmbeddings();
const vectorStore = await FaissStore.fromDocuments(splitDocs, embeddings);
const directory = "../db/vector";
await vectorStore.save(directory);
};
run();
运行上述文件后 , 生成 docstore.json 和二进制文件 faiss.index
docstore.json 中 , 即向量化后的数据 :
retriever
从向量数据库中检索 , 我提问 : 日本怎么称呼我们中国? , 将从向量数据中检索 ,
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import "faiss-node";
import dotenv from "dotenv";
dotenv.config();
async function f() {
const directory = "../db/vector";
const embeddings = new OpenAIEmbeddings(
{
modelName: "text-embedding-ada-002", //指定模型的名称
maxConcurrency: 10, //设置最大的并发数 , 意味着同负一时间最多可以并行处理10个请求 , 避免过多并发请求 , 导致系统过载和api限流
maxRetries: 3, //设置最大的重试次数 , 当api调用失败的时候 , 程序会自动重试最多三次 , 这增加请求成功的概率 , 提高了系统的可靠性
},
{
batchSize: 100, //设置批量处理的大小 , 每次调用api 最多处理100个文本片段 , 但同时也要注意api的限制和内存的使用
}
);
//加载向量储存
const vectorstore = await FaissStore.load(directory, embeddings);
//从向量数据库中创建一个检索器
const retriever = vectorstore.asRetriever(2);
//使用Runnable API进行进行检索
const res = await retriever.invoke("日本怎么称呼我们中国?");
console.log(res);
}
f();
结果如下 :
总结
学到这里 , 我已经知道知识库从自然语言到向量的过程 , 从数据角度的话 , 经历了一下过程 :
- 加载数据源
- 分割数据
- 向量化数据
- 持久化数据
逐步走向 RAG ~
Vue 项目开发全攻略:从搭建到上线的技术盛宴
一、项目搭建
在开始开发 Vue 项目时,首先要进行项目搭建。这里我们选用 vite 来负责工程化,它能极大地提升项目构建和开发的效率。
使用 vite 搭建 Vue 项目非常简单,只需在命令行中输入 npm init vite 这一指令,就能快速初始化一个全新的 Vue 项目框架。vite 是新一代的前端构建工具,它基于 ES 模块导入,在开发环境下无需打包操作,可直接启动开发服务器,实现快速冷启动。在生产环境中,vite 又能利用 Rollup 进行高效的打包,为项目提供优化后的代码输出。通过这种方式,我们能轻松搭建起一个基础的 Vue 项目架构,为后续的开发工作奠定坚实的基础。
二、核心技术栈
2.1 Vue 核心语法
Vue 的核心语法是构建项目的基石 ,在本项目中,响应式原理通过ref和reactive两个函数来实现。例如,当需要创建一个简单的响应式数据时,使用ref函数:
import { ref } from 'vue';
const count = ref(0);
若要处理复杂的对象或数组,reactive则更为合适:
import { reactive } from 'vue';
const userInfo = reactive({
name: 'John',
age: 30
});
组件化开发让代码的可维护性和复用性大大提高。在项目里,我们将页面拆分成多个组件,每个组件都有独立的逻辑和视图。以一个按钮组件为例,其template部分定义了按钮的外观:
<template>
<button>{{ buttonText }}</button>
</template>
script部分则负责组件的逻辑,如:
<script setup>
import { ref } from 'vue';
const buttonText = ref('点击我');
</script>
指令方面,v - if、v - show用于控制元素的显示与隐藏。v - for则常用于列表的渲染,假设我们有一个用户列表:
const userList = reactive([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
在模板中使用v - for进行渲染:
<template>
<ul>
<li v - for="user in userList" :key="user.id">{{ user.name }}</li>
</ul>
</template>
通过这些核心语法的运用,我们能够构建出灵活且高效的 Vue 应用程序。
2.2 Vue - Router 路由
在 Vue - Router 的配置中,多级路由的设置让页面结构更加清晰。例如,我们有一个主页面Home,其下包含About和Contact两个子页面。在路由配置文件中可以这样定义:
import { createRouter, createWebHistory } from 'vue - router';
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/home',
component: Home,
children: [
{ path: 'about', component: About },
{ path: 'contact', component: Contact }
]
}
]
});
路由懒加载是提升性能的关键。我们使用import()函数来实现,当访问特定路由时,对应的组件才会被加载。比如:
const About = () => import('./views/About.vue');
添加路由守卫则能有效控制页面的访问权限。以登录验证为例,在全局前置守卫中可以这样实现:
router.beforeEach((to, from, next) => {
const isLoggedIn = localStorage.getItem('token');
if (to.meta.requiresAuth &&!isLoggedIn) {
next('/login');
} else {
next();
}
});
在需要验证的路由中,设置meta字段:
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
}
通过这样的配置,确保了只有登录用户才能访问受保护的页面。
2.3 Pinia 状态管理
Pinia 在项目中负责状态的管理,极大地简化了状态共享的过程。首先,安装 Pinia 并在main.js中进行配置:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');
接着,定义一个store来管理用户相关的状态。例如:
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
isLoggedIn: false
}),
actions: {
login(user) {
this.userInfo = user;
this.isLoggedIn = true;
localStorage.setItem('token', 'valid - token');
},
logout() {
this.userInfo = null;
this.isLoggedIn = false;
localStorage.removeItem('token');
}
}
});
在组件中使用该store时,只需引入并调用相应的方法:
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
userStore.login({ name: 'John', age: 30 });
Pinia 的优势在于其简洁的 API 和良好的模块化设计,使得状态管理变得轻松且高效。
2.4 Element3 UI 组件库
Element3 是一个功能强大的 UI 组件库,为项目提供了丰富的组件。在使用时,我们采用按需加载的方式来优化性能。首先,安装相关的插件:
npm install -D unplugin - vue - components unplugin - auto - import
然后,在vue.config.js中进行配置:
const AutoImport = require('unplugin - auto - import/webpack');
const Components = require('unplugin - vue - components/webpack');
const { ElementPlusResolver } = require('unplugin - vue - components/resolvers');
module.exports = {
configureWebpack: {
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
}
};
这样,在组件中使用 Element3 组件时,如按钮组件,只需直接引入:
<template>
<el - button type="primary">点击我</el - button>
</template>
Element3 组件以el -开头,通过按需加载,我们避免了引入不必要的组件,有效减少了项目的打包体积,提升了页面的加载速度。
2.5 Stylus CSS 预处理器
Stylus 作为 CSS 预处理器,为项目带来了诸多便利。它允许我们使用变量、混入、嵌套等功能,使 CSS 代码更加简洁和易于维护。例如,定义一个颜色变量:
$primaryColor = #1890ff
在样式中使用该变量:
button {
background - color: $primaryColor;
color: white;
}
混入功能可以复用一些常用的样式,如圆角样式:
border - radius() {
border - radius: 5px;
}
.box {
+border - radius();
}
样式的嵌套则让代码结构更加清晰,以导航栏为例:
.nav {
display: flex;
justify - content: space - between;
li {
list - style: none;
a {
text - decoration: none;
color: #333;
&:hover {
color: $primaryColor;
}
}
}
}
通过 Stylus 的这些特性,我们能够高效地编写和管理项目的样式。
2.6 Axios AJAX 请求封装库
Axios 用于与后端进行数据交互,我们对其进行了封装,以提高代码的复用性和可维护性。首先,创建一个api.js文件,设置基础 URL 和请求拦截器:
import axios from 'axios';
const service = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000
});
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
然后,封装常用的请求方法,如get和post:
export const get = (url, params = {}) => {
return service.get(url, { params });
};
export const post = (url, data = {}) => {
return service.post(url, data);
};
在组件中使用时,只需引入相应的方法:
import { get } from '@/api';
get('/user/info').then(response => {
console.log(response.data);
}).catch(error => {
console.error(error);
});
通过这样的封装,我们能够方便地进行各种 AJAX 请求,与后端进行稳定的数据交互。
三、项目亮点展示
3.1 ES6 风格的全面应用
在整个项目中,我们全面采用了 ES6 风格的代码编写方式,这使得代码在简洁性、易读性和易维护性上都有了显著提升。例如,在定义路由时,使用对象解构的方式简化了代码结构。原本需要完整书写routes: routes,现在直接写成routes即可。这种简洁的写法不仅减少了冗余代码,还让代码逻辑更加清晰,开发者能够一眼看清路由的配置关系。
在函数定义方面,ES6 的箭头函数也被广泛应用。比如在处理一些简单的回调函数时,箭头函数的使用使得代码更加紧凑。例如,在数组的map方法中,使用箭头函数可以快速对数组中的每个元素进行处理:
const numbers = [1, 2, 3, 4];
const squaredNumbers = numbers.map((number) => number * number);
相比于传统的函数定义方式,箭头函数的语法更加简洁,同时也避免了this指向的问题,让代码的维护更加轻松。
3.2 良好的注释与代码可读性
良好的注释是提高代码可读性的关键。在项目中,我们在关键的代码块、函数定义以及复杂的逻辑处都添加了详细的注释。例如,在路由守卫的代码中,我们添加了注释来说明其作用和逻辑:
// 全局前置守卫,用于验证用户是否登录
router.beforeEach((to, from, next) => {
const isLoggedIn = localStorage.getItem('token');
if (to.meta.requiresAuth &&!isLoggedIn) {
// 如果目标路由需要登录且用户未登录,则重定向到登录页面
next('/login');
} else {
// 否则,允许用户访问目标路由
next();
}
});
这样的注释使得其他开发者在阅读代码时,能够快速理解代码的意图和功能,降低了代码的理解成本。同时,对于一些自定义的函数和组件,我们也添加了注释来解释其输入参数、返回值以及功能用途,确保代码的每一部分都清晰易懂。
3.3 规范的 Git 提交记录和习惯
在项目开发过程中,我们始终保持着规范的 Git 提交记录和良好的提交习惯。每次提交都有明确的提交信息,描述本次提交所做的修改内容。例如,“修复登录页面的验证码验证问题”“优化首页的加载速度” 等。这样的提交信息使得项目的版本历史清晰可追溯,团队成员能够快速了解每个提交的目的和影响范围。
同时,我们遵循一定的分支管理策略,如使用master分支作为主分支,用于发布稳定版本;develop分支用于开发新功能,通过创建特性分支进行功能开发,开发完成后再合并到develop分支。这种规范的分支管理和提交习惯,不仅有助于团队协作开发,还能在出现问题时快速定位和解决,提高了项目的开发效率和质量。
四、实战技巧与注意事项
4.1 表单组件的使用
在项目中,表单组件的使用非常频繁。我们使用 :model来收集表单数据,这是一种双向数据绑定的方式,能够实时同步表单输入与数据模型。例如:
<el - form :model="formData">
<el - form - item label="用户名">
<el - input v - model="formData.username"></el - input>
</el - form - item>
<el - form - item label="密码">
<el - input type="password" v - model="formData.password"></el - input>
</el - form - item>
</el - form>
在上述代码中,formData是一个包含username和password字段的对象,通过v - model指令,表单输入框的值会实时更新到formData中,反之亦然。
通过ref可以获取表单实例,这在需要手动操作表单时非常有用。在模板中,使用ref标记表单组件:
<el - form ref="formRef" :model="formData">
<!-- 表单内容 -->
</el - form>
在script部分,通过ref获取表单实例:
import { ref } from 'vue';
const formRef = ref(null);
当表单挂载后,formRef就会获取到实际的表单实例。此时,我们可以调用表单实例的方法,如validate方法进行表单校验:
formRef.value.validate((valid) => {
if (valid) {
// 校验通过,提交表单或执行其他操作
console.log('表单校验通过');
} else {
// 校验失败,提示用户错误信息
console.log('表单校验失败');
}
});
表单的校验规则通过rules属性来定义。例如,对用户名和密码设置必填校验:
const formData = reactive({
username: '',
password: ''
});
const rules = {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
]
};
在表单组件中,将rules属性绑定到对应的form - item上:
<el - form :model="formData" :rules="rules">
<el - form - item label="用户名" prop="username">
<el - input v - model="formData.username"></el - input>
</el - form - item>
<el - form - item label="密码" prop="password">
<el - input type="password" v - model="formData.password"></el - input>
</el - form - item>
</el - form>
这样,当用户输入完成并离开输入框(blur事件触发)时,表单会根据设置的规则进行校验,并显示相应的错误提示信息。
4.2 布局组件的应用
布局组件在构建页面结构时起着关键作用。我们常用的布局组件包括Elcontainer、Elheader、ElAside、ElMain等。
以一个常见的后台管理页面布局为例,使用Elcontainer作为容器,将页面分为头部、侧边栏和主体内容区域:
<el - container>
<el - header>
<!-- 头部内容,如导航栏 -->
<h1>后台管理系统</h1>
</el - header>
<el - container>
<el - aside width="200px">
<!-- 侧边栏菜单 -->
<el - menu :default - active="activeIndex" class="el - menu - vertical - demo" @select="handleSelect">
<el - menu - item index="1">菜单1</el - menu - item>
<el - menu - item index="2">菜单2</el - menu - item>
</el - menu>
</el - aside>
<el - main>
<!-- 主体内容区域 -->
<p>这里是主要内容</p>
</el - main>
</el - container>
</el - container>
在上述代码中,Elheader定义了页面的头部,通常包含导航栏等信息。Elaside作为侧边栏,设置了固定的宽度为200px,并在其中放置了菜单组件。Elmain则用于展示主体内容。
对于页面内的布局,ElRow和ElCol经常被用于实现栅格化布局。例如,将一行分为两列,左列占 8 格,右列占 4 格:
<el - row>
<el - col :span="8">
<p>左列内容</p>
</el - col>
<el - col :span="4">
<p>右列内容</p>
</el - col>
</el - row>
通过span属性可以灵活调整每列所占的比例,从而实现各种复杂的页面布局。
4.3 性能优化策略
在项目开发过程中,性能优化至关重要。我们采用了多种策略来提升项目的性能。
按需加载是其中一个重要的策略。在引入 Vue 组件库 Element3 时,我们通过配置实现了按需加载,避免一次性加载所有组件,从而减少了初始加载时间。在路由方面,也采用了懒加载技术,只有当用户访问特定路由时,对应的组件才会被加载。例如:
const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
});
这样,在应用启动时,只有必要的路由组件会被加载,大大提高了应用的启动速度。
此外,还对静态资源进行了优化。通过使用 Webpack 插件对 JavaScript 和 CSS 文件进行压缩,减少了文件体积,加快了文件的传输速度。同时,合理利用缓存机制,对于不经常变化的静态资源设置较长的缓存时间,避免用户每次访问都重新下载。
在图片处理方面,对图片进行了压缩和格式优化,选择合适的图片格式(如 WebP 格式,在保证图片质量的前提下,文件体积更小),并根据不同的设备屏幕尺寸提供相应分辨率的图片,避免加载过大的图片资源,从而提升页面的加载速度和用户体验。
五、总结与展望
通过本次项目的开发,我收获了许多宝贵的经验。从项目搭建到技术栈的运用,再到项目亮点的打造和实战技巧的积累,每一个环节都让我对 Vue 开发有了更深入的理解。在项目中,我学会了如何高效地运用各种工具和技术,解决实际开发中遇到的问题。同时,也深刻体会到团队协作、代码规范以及性能优化的重要性。
展望未来,我希望能够进一步优化项目。在性能方面,持续探索更有效的优化策略,如进一步优化图片加载、减少 HTTP 请求等,以提升用户体验。在功能上,根据用户反馈和业务需求,不断添加新的功能模块,使项目更加完善。同时,也会关注 Vue 技术的发展动态,及时引入新的特性和最佳实践,保持项目的技术先进性。
Tauri(五)——实现托盘菜单和图标切换功能
前言
在桌面端应用中,托盘图标是常见的功能,本文将以 Tauri V2 框架为例,展示如何实现托盘菜单以及根据主题切换托盘图标的功能。以下是效果截图和详细实现步骤和代码说明。
1. 修改 Cargo.toml
添加依赖
首先,在 src-tauri/Cargo.toml
文件中添加如下依赖:
[dependencies]
tauri = { version = "2.0.6", features = ["tray-icon", "image-png"] }
-
tray-icon
: 启用托盘图标功能。 -
image-png
: 支持自定义 PNG 图标。
2. 实现托盘菜单功能
在 Rust 中,我们创建一个 enable_tray
函数,用于初始化托盘菜单及其事件。
enable_tray
函数
fn enable_tray(app: &mut tauri::App) {
use tauri::{
image::Image,
menu::{MenuBuilder, MenuItem},
tray::TrayIconBuilder,
};
// 退出按钮
let quit_i = MenuItem::with_id(app, "quit", "Quit Coco", true, None::<&str>).unwrap();
// 设置按钮
let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, None::<&str>).unwrap();
// 打开按钮
let open_i = MenuItem::with_id(app, "open", "Open Coco", true, None::<&str>).unwrap();
// 关于按钮
let about_i = MenuItem::with_id(app, "about", "About Coco", true, None::<&str>).unwrap();
// 隐藏按钮
let hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap();
// ......
// 按照一定顺序 把按钮 放到 菜单里
let menu = MenuBuilder::new(app)
.item(&open_i)
.separator() // 分割线
.item(&hide_i)
.item(&about_i)
.item(&settings_i)
.separator() // 分割线
.item(&quit_i)
.build()
.unwrap();
let _tray = TrayIconBuilder::with_id("tray")
// .icon(app.default_window_icon().unwrap().clone()) // 默认的图片
.icon(Image::from_bytes(include_bytes!("../icons/light@2x.png")).expect("REASON")) // 自定义的图片
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"open" => {
handle_open_coco(app); // 打开事件
}
"hide" => {
handle_hide_coco(app);
}
"about" => {
let _ = app.emit("open_settings", "about");
}
"settings" => {
// windows failed to open second window, issue: https://github.com/tauri-apps/tauri/issues/11144 https://github.com/tauri-apps/tauri/issues/8196
//#[cfg(windows)]
let _ = app.emit("open_settings", "");
// #[cfg(not(windows))]
// open_settings(&app);
}
"quit" => {
println!("quit menu item was clicked");
app.exit(0);
}
_ => {
println!("menu item {:?} not handled", event.id);
}
})
.build(app)
.unwrap();
}
功能说明
-
菜单项创建:使用
MenuItem::with_id
方法创建菜单项并设置唯一 ID 和显示文本。 -
菜单构建:通过
MenuBuilder
组合菜单项并添加分隔符。 -
托盘图标构建:通过
TrayIconBuilder
设置图标、菜单及点击事件。 -
事件监听:在
on_menu_event
中根据菜单项 ID 处理对应事件。
3. 注册托盘菜单
在 Tauri 应用启动时,调用 enable_tray
注册托盘菜单。
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut ctx = tauri::generate_context!();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
switch_tray_icon, // 切换托盘图标
])
.setup(|app| {
init(app.app_handle());
enable_tray(app); // 注册事件
Ok(())
})
.run(ctx)
.expect("error while running tauri application");
}
4. 实现托盘图标切换
为了根据主题切换托盘图标,我们需要创建一个 switch_tray_icon
命令。
switch_tray_icon
命令
#[tauri::command]
fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
let app_handle = app.app_handle();
println!("is_dark_mode: {}", is_dark_mode);
const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");
// 根据 app 的主题切换 图标
let icon_path: &[u8] = if is_dark_mode {
DARK_ICON_PATH
} else {
LIGHT_ICON_PATH
};
// 获取托盘
let tray = match app_handle.tray_by_id("tray") {
Some(tray) => tray,
None => {
eprintln!("Tray with ID 'tray' not found");
return;
}
};
// 设置图标
if let Err(e) = tray.set_icon(Some(
tauri::image::Image::from_bytes(icon_path)
.unwrap_or_else(|e| panic!("Failed to load icon from bytes: {}", e)),
)) {
eprintln!("Failed to set tray icon: {}", e);
}
}
代码说明
-
动态加载图标:根据
is_dark_mode
参数决定使用亮色或暗色图标。 -
更新托盘图标:通过
set_icon
方法更新图标。 - 错误处理:在托盘实例不存在或图标加载失败时记录错误日志。
5. 前端调用 Rust 命令
前端可以通过 Tauri 的 invoke
API 调用 switch_tray_icon
命令。
示例代码
import { invoke } from "@tauri-apps/api/core";
async function switchTrayIcon(value: "dark" | "light") {
try {
// invoke switch_tray_icon 事件名 isDarkMode 参数名
await invoke("switch_tray_icon", { isDarkMode: value === "dark" });
} catch (err) {
console.error("Failed to switch tray icon:", err);
}
}
在主题切换时调用 switchTrayIcon
即可实现图标动态切换。
小结
通过本文的实现,我们完成了以下功能:
- 创建自定义托盘菜单。(更丰富的菜单内容可以自行扩展了)
- 响应托盘菜单事件。
- 根据主题动态切换托盘图标。(不仅仅可以主题切换图标,还可以依据 app 行为修改对应的图标)
这种方式为 Tauri 应用提供了更加友好的用户体验。如果有其他需求,可以在菜单事件中扩展更多功能。
参考
开源
最近,我正在基于 Tauri 开发一款项目,名为 Coco。目前已开源,项目仍在不断完善中,欢迎大家前往支持并为项目点亮免费的 star 🌟!
作为个人的第一个 Tauri 项目,开发过程中也是边探索边学习。希望能与志同道合的朋友一起交流经验、共同成长!
代码中如有问题或不足之处,期待小伙伴们的宝贵建议和指导!
- 官网: coco.rs/
- 前端仓库: github.com/infinilabs/…
- 服务端仓库: github.com/infinilabs/…
非常感谢您的支持与关注!