普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月11日首页

📘 全面解析:JavaScript 时间格式化 API 实战指南

作者 excel
2025年11月11日 07:31

——从 toUTCString()Intl.DateTimeFormat 的深度理解

时间与日期,是前端开发中最容易“踩坑”的部分。不同浏览器、不同区域、甚至不同系统语言,都会造成输出不一致。
本文将系统解析 JavaScript 提供的时间格式化方法,帮你彻底搞懂它们的差异、用途与正确使用方式。


一、背景:为什么会有这么多时间格式化方法?

JavaScript 的时间系统基于 ECMAScript 时间标准,时间点以 UTC 为基准(Unix Epoch:1970-01-01 00:00:00 UTC)。
但由于前端代码运行在不同地域的用户设备中,所以:

  • 一部分方法输出国际标准格式(机器可读);
  • 一部分方法输出本地化格式(用户可读);
  • 一部分方法支持自定义区域、时区和格式

二、核心 API 总览表

方法 时区 是否本地化 是否可定制 输出示例 主要用途
toString() 本地时间 "Tue Nov 11 2025 17:00:00 GMT+0800 (China Standard Time)" 调试/默认输出
toUTCString() UTC "Tue, 11 Nov 2025 09:00:00 GMT" 标准化输出(日志/HTTP)
toGMTString() UTC "Tue, 11 Nov 2025 09:00:00 GMT" 历史遗留(不推荐)
toISOString() UTC "2025-11-11T09:00:00.000Z" 数据交换(JSON/HTTP)
toLocaleString() 本地时区 ✅ 是 ✅ 是 "2025/11/11 17:00:00" 用户界面显示
Intl.DateTimeFormat 任意指定 ✅ 是 ✅ 是 "11 November 2025, 17:00:00" 完全可控本地化输出

三、逐个详解与代码示例


toUTCString()

用途:输出 UTC 时间的 RFC1123 标准格式
语法

date.toUTCString();

示例:

const d = new Date('2025-11-11T09:00:00Z');
console.log(d.toUTCString());
// "Tue, 11 Nov 2025 09:00:00 GMT"

特性:

  • 输出固定格式,不可配置;
  • 常用于日志、HTTP Header;
  • 不受系统区域影响。

适用场景:

// 设置 HTTP 响应头
response.setHeader('Expires', new Date(Date.now() + 3600000).toUTCString());

toGMTString()(已废弃)

用途toUTCString() 的旧版本,早期 Netscape/IE 兼容接口。
语法:

date.toGMTString();

说明:

  • 在现代浏览器中行为与 toUTCString() 相同;
  • ECMAScript 已标记为 deprecated
  • 不推荐使用,仅兼容老项目。

toLocaleString()

用途:输出与用户地区语言相符的本地化时间字符串。
语法:

date.toLocaleString([locales], [options]);

参数说明:

参数 类型 说明
locales string 或 string[] 语言代码(如 'zh-CN', 'en-US'
options object 格式化选项

常用选项:

{
  timeZone: 'Asia/Shanghai', // 指定时区
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  hour12: false
}

示例:

const d = new Date('2025-11-11T09:00:00Z');
console.log(d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }));
// "2025/11/11 17:00:00"

console.log(d.toLocaleString('en-US', { timeZone: 'America/New_York' }));
// "11/11/2025, 4:00:00 AM"

可本地化、可控、最灵活。


toISOString()

用途:输出 ISO 8601 标准的 UTC 时间字符串。
语法:

date.toISOString();

输出示例:

new Date('2025-11-11T09:00:00Z').toISOString();
// "2025-11-11T09:00:00.000Z"

特性:

  • 输出固定格式;
  • 精确到毫秒;
  • 常用于 JSON / 数据交换;
  • Node.js 和后端系统高度兼容。

示例:

JSON.stringify({ createdAt: new Date().toISOString() });

Intl.DateTimeFormat

用途:提供强大的国际化时间格式化能力。
语法:

new Intl.DateTimeFormat(locales, options).format(date);

示例:

const d = new Date('2025-11-11T09:00:00Z');
const fmt = new Intl.DateTimeFormat('en-GB', {
  timeZone: 'UTC',
  dateStyle: 'full',
  timeStyle: 'long'
});
console.log(fmt.format(d));
// "Tuesday, 11 November 2025 at 09:00:00 GMT"

更细粒度控制:

const custom = new Intl.DateTimeFormat('zh-CN', {
  timeZone: 'Asia/Shanghai',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  weekday: 'long',
  hour: '2-digit',
  minute: '2-digit'
});
console.log(custom.format(d));
// "2025年11月11日星期二 17:00"

✅ 支持:

  • 时区切换;
  • 多语言;
  • 长短日期格式;
  • 星期显示;
  • 本地化数字。

四、对比实测:同一个时间,不同输出

const d = new Date('2025-11-11T09:00:00Z');

console.log(d.toString());         // "Tue Nov 11 2025 17:00:00 GMT+0800 (China Standard Time)"
console.log(d.toUTCString());      // "Tue, 11 Nov 2025 09:00:00 GMT"
console.log(d.toISOString());      // "2025-11-11T09:00:00.000Z"
console.log(d.toLocaleString());   // "2025/11/11 17:00:00"
console.log(
  new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' }).format(d)
);                                 // "11/11/2025"

📊 总结图示:

方法 时区 输出可定制 推荐用途
toUTCString() UTC 机器可读、HTTP Header
toGMTString() UTC 已废弃
toLocaleString() 本地时区 用户界面展示
toISOString() UTC 数据序列化、存储
Intl.DateTimeFormat 任意 ✅✅✅ 最强国际化控制

五、潜在问题与实战建议

问题 说明 建议
不同浏览器格式差异 特别是 toLocaleString() 显式指定 locale + timeZone
服务器和客户端时区不一致 SSR 输出 UTC、CSR 输出本地 统一 timeZone(如 'UTC' 或 'Asia/Shanghai')
想统一格式输出 toUTCString() 太固定 改用 Intl.DateTimeFormatdayjs
移动端差异 ICU 版本不同 避免依赖系统默认格式

六、实践案例:统一格式化函数封装

function formatDate(date, opts = {}) {
  const {
    locale = 'zh-CN',
    timeZone = 'Asia/Shanghai',
    options = {}
  } = opts;

  const fmt = new Intl.DateTimeFormat(locale, {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
    ...options
  });
  return fmt.format(date);
}

console.log(formatDate(new Date(), { timeZone: 'UTC' }));
// 输出如:2025/11/11 09:00:00

七、结语:选择建议总结

场景 推荐 API
机器通信 / JSON / HTTP toISOString() / toUTCString()
用户界面显示(国际化) toLocaleString()Intl.DateTimeFormat
跨区域一致性(前后端) 固定使用 UTC + 格式化控制
需要灵活格式 使用 Intl.DateTimeFormatdayjs

一句话总结:

  • toUTCString():标准化 UTC 文本输出
  • toLocaleString():本地化用户界面输出
  • toISOString():数据传输标准输出
  • Intl.DateTimeFormat:国际化与自定义格式首选

本文部分内容借助 AI 辅助生成,并由作者整理审核。

FAA限制美国12个主要空港的私人飞机运营

2025年11月11日 07:30
由于空中交通管制员短缺,美国联邦航空管理局对肯尼迪国际机场、洛杉矶国际机场和奥黑尔国际机场等机场的大部分商务航空实施限制,此举是在强制航空公司削减航班计划基础上的进一步措施。周末美国航班混乱情况加剧,取消和延误次数超过数千架次,波及美国航空(AAL)、达美航空(DAL)和美联航(UAL)等航空公司。(新浪财经)

Vue组件通信不再难!这8种方式让你彻底搞懂父子兄弟传值

2025年11月11日 07:29

你是不是经常遇到这样的场景?父组件的数据要传给子组件,子组件的事件要通知父组件,兄弟组件之间要共享状态...每次写Vue组件通信都觉得头大,不知道用哪种方式最合适?

别担心!今天我就带你彻底搞懂Vue组件通信的8种核心方式,每种方式都有详细的代码示例和适用场景分析。看完这篇文章,你就能根据具体业务场景选择最合适的通信方案,再也不用为组件间传值发愁了!

Props:最基础的父子通信

Props是Vue中最基础也是最常用的父子组件通信方式。父组件通过属性向下传递数据,子组件通过props选项接收。

// 子组件 ChildComponent.vue
<template>
  <div>
    <h3>子组件接收到的消息:{{ message }}</h3>
    <p>用户年龄:{{ userInfo.age }}</p>
  </div>
</template>

<script>
export default {
  // 定义props,可以指定类型和默认值
  props: {
    message: {
      type: String,
      required: true  // 必须传递这个prop
    },
    userInfo: {
      type: Object,
      default: () => ({})  // 默认空对象
    }
  }
}
</script>

// 父组件 ParentComponent.vue
<template>
  <div>
    <child-component 
      :message="parentMessage" 
      :user-info="userData"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: '来自父组件的问候',
      userData: {
        name: '小明',
        age: 25
      }
    }
  }
}
</script>

适用场景:简单的父子组件数据传递,数据流清晰明确。但要注意,props是单向数据流,子组件不能直接修改props。

$emit:子组件向父组件通信

当子组件需要向父组件传递数据或触发父组件的某个方法时,就需要用到$emit。子组件通过触发自定义事件,父组件通过v-on监听这些事件。

// 子组件 SubmitButton.vue
<template>
  <button @click="handleClick">
    提交表单
  </button>
</template>

<script>
export default {
  methods: {
    handleClick() {
      // 触发自定义事件,并传递数据
      this.$emit('form-submit', {
        timestamp: new Date(),
        formData: this.formData
      })
    }
  }
}
</script>

// 父组件 FormContainer.vue
<template>
  <div>
    <submit-button @form-submit="handleFormSubmit" />
  </div>
</template>

<script>
import SubmitButton from './SubmitButton.vue'

export default {
  components: { SubmitButton },
  methods: {
    handleFormSubmit(payload) {
      console.log('接收到子组件的数据:', payload)
      // 这里可以处理表单提交逻辑
      this.submitToServer(payload.formData)
    }
  }
}
</script>

适用场景:子组件需要通知父组件某个事件发生,或者需要传递数据给父组件处理。

ref:直接访问子组件实例

通过ref属性,父组件可以直接访问子组件的实例,调用其方法或访问其数据。

// 子组件 CustomInput.vue
<template>
  <input 
    ref="inputRef" 
    v-model="inputValue" 
    type="text"
  />
</template>

<script>
export default {
  data() {
    return {
      inputValue: ''
    }
  },
  methods: {
    // 子组件的自定义方法
    focus() {
      this.$refs.inputRef.focus()
    },
    clear() {
      this.inputValue = ''
    },
    getValue() {
      return this.inputValue
    }
  }
}
</script>

// 父组件 ParentComponent.vue
<template>
  <div>
    <custom-input ref="myInput" />
    <button @click="handleFocus">聚焦输入框</button>
    <button @click="handleClear">清空输入框</button>
  </div>
</template>

<script>
import CustomInput from './CustomInput.vue'

export default {
  components: { CustomInput },
  methods: {
    handleFocus() {
      // 通过ref直接调用子组件的方法
      this.$refs.myInput.focus()
    },
    handleClear() {
      // 调用子组件的清空方法
      this.$refs.myInput.clear()
      
      // 也可以直接访问子组件的数据(不推荐)
      // this.$refs.myInput.inputValue = ''
    }
  }
}
</script>

适用场景:需要直接操作子组件的DOM元素或调用子组件方法的场景。但要谨慎使用,避免破坏组件的封装性。

Event Bus:任意组件间通信

Event Bus通过创建一个空的Vue实例作为事件中心,实现任意组件间的通信,特别适合非父子组件的情况。

// event-bus.js - 创建事件总线
import Vue from 'vue'
export const EventBus = new Vue()

// 组件A - 事件发送者
<template>
  <button @click="sendMessage">发送全局消息</button>
</template>

<script>
import { EventBus } from './event-bus'

export default {
  methods: {
    sendMessage() {
      // 触发全局事件
      EventBus.$emit('global-message', {
        text: 'Hello from Component A!',
        from: 'ComponentA'
      })
    }
  }
}
</script>

// 组件B - 事件监听者
<template>
  <div>
    <p>最新消息:{{ latestMessage }}</p>
  </div>
</template>

<script>
import { EventBus } from './event-bus'

export default {
  data() {
    return {
      latestMessage: ''
    }
  },
  mounted() {
    // 监听全局事件
    EventBus.$on('global-message', (payload) => {
      this.latestMessage = `${payload.from} 说:${payload.text}`
    })
  },
  beforeDestroy() {
    // 组件销毁前移除事件监听,防止内存泄漏
    EventBus.$off('global-message')
  }
}
</script>

适用场景:简单的跨组件通信,小型项目中的状态管理。但在复杂项目中建议使用Vuex或Pinia。

provide/inject:依赖注入

provide和inject主要用于高阶组件开发,允许祖先组件向其所有子孙后代注入依赖,而不需要层层传递props。

// 祖先组件 Ancestor.vue
<template>
  <div>
    <middle-component />
  </div>
</template>

<script>
export default {
  // 提供数据和方法
  provide() {
    return {
      // 提供响应式数据
      appTheme: this.theme,
      // 提供方法
      changeTheme: this.changeTheme,
      // 提供常量
      appName: '我的Vue应用'
    }
  },
  data() {
    return {
      theme: 'dark'
    }
  },
  methods: {
    changeTheme(newTheme) {
      this.theme = newTheme
    }
  }
}
</script>

// 深层子组件 DeepChild.vue
<template>
  <div :class="`theme-${appTheme}`">
    <h3>应用名称:{{ appName }}</h3>
    <button @click="changeTheme('light')">切换亮色主题</button>
    <button @click="changeTheme('dark')">切换暗色主题</button>
  </div>
</template>

<script>
export default {
  // 注入祖先组件提供的数据
  inject: ['appTheme', 'changeTheme', 'appName'],
  
  // 也可以指定默认值和来源
  // inject: {
  //   theme: {
  //     from: 'appTheme',
  //     default: 'light'
  //   }
  // }
}
</script>

适用场景:组件层级很深,需要避免props逐层传递的麻烦。常用于开发组件库或大型应用的基础配置。

attrs/attrs/listeners:跨层级属性传递

attrs包含了父组件传入的所有非props属性,attrs包含了父组件传入的所有非props属性,listeners包含了父组件传入的所有事件监听器,可以用于创建高阶组件。

// 中间组件 MiddleComponent.vue
<template>
  <div>
    <!-- 传递所有属性和事件到子组件 -->
    <child-component v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  inheritAttrs: false, // 不让属性绑定到根元素
  mounted() {
    console.log('$attrs:', this.$attrs) // 所有非props属性
    console.log('$listeners:', this.$listeners) // 所有事件监听器
  }
}
</script>

// 最终子组件 ChildComponent.vue
<template>
  <input
    v-bind="$attrs"
    v-on="$listeners"
    class="custom-input"
  />
</template>

<script>
export default {
  mounted() {
    // 可以直接使用父组件传递的所有属性和事件
    console.log('接收到的属性:', this.$attrs)
  }
}
</script>

// 父组件使用
<template>
  <middle-component
    placeholder="请输入内容"
    maxlength="20"
    @focus="handleFocus"
    @blur="handleBlur"
  />
</template>

适用场景:创建包装组件、高阶组件,需要透传属性和事件的场景。

Vuex:集中式状态管理

Vuex是Vue的官方状态管理库,适用于中大型复杂应用的状态管理。

// store/index.js
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      count: 0,
      user: null,
      loading: false
    }
  },
  mutations: {
    // 同步修改状态
    increment(state) {
      state.count++
    },
    setUser(state, user) {
      state.user = user
    },
    setLoading(state, loading) {
      state.loading = loading
    }
  },
  actions: {
    // 异步操作
    async login({ commit }, credentials) {
      commit('setLoading', true)
      try {
        const user = await api.login(credentials)
        commit('setUser', user)
        return user
      } finally {
        commit('setLoading', false)
      }
    }
  },
  getters: {
    // 计算属性
    isLoggedIn: state => !!state.user,
    doubleCount: state => state.count * 2
  }
})

// 组件中使用
<template>
  <div>
    <p>计数器:{{ count }}</p>
    <p>双倍计数:{{ doubleCount }}</p>
    <p v-if="isLoggedIn">欢迎,{{ user.name }}!</p>
    <button @click="increment">增加</button>
    <button @click="login" :disabled="loading">
      {{ loading ? '登录中...' : '登录' }}
    </button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // 映射state和getters到计算属性
    ...mapState(['count', 'user', 'loading']),
    ...mapGetters(['doubleCount', 'isLoggedIn'])
  },
  methods: {
    // 映射mutations和actions到方法
    ...mapMutations(['increment']),
    ...mapActions(['login'])
  }
}
</script>

适用场景:中大型复杂应用,多个组件需要共享状态,需要严格的状态管理流程。

Pinia:新一代状态管理

Pinia是Vue官方推荐的新一代状态管理库,相比Vuex更加轻量、直观,并且完美支持TypeScript。

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '我的计数器'
  }),
  
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用this访问其他getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  
  actions: {
    increment() {
      this.count++
    },
    async incrementAsync() {
      // 异步action
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    },
    reset() {
      this.count = 0
    }
  }
})

// 组件中使用
<template>
  <div>
    <h3>{{ name }}</h3>
    <p>当前计数:{{ count }}</p>
    <p>双倍计数:{{ doubleCount }}</p>
    <button @click="increment">增加</button>
    <button @click="incrementAsync">异步增加</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script>
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counterStore = useCounterStore()
    
    // 可以直接解构,但会失去响应式
    // 使用storeToRefs保持响应式
    const { name, count, doubleCount } = counterStore
    
    return {
      // state和getters
      name,
      count,
      doubleCount,
      // actions
      increment: counterStore.increment,
      incrementAsync: counterStore.incrementAsync,
      reset: counterStore.reset
    }
  }
}
</script>

// 在多个store之间交互
import { useUserStore } from '@/stores/user'

export const useCartStore = defineStore('cart', {
  actions: {
    async checkout() {
      const userStore = useUserStore()
      
      if (!userStore.isLoggedIn) {
        await userStore.login()
      }
      
      // 结账逻辑...
    }
  }
})

适用场景:现代Vue应用的状态管理,特别是需要TypeScript支持和更简洁API的项目。

实战场景选择指南

现在你已经了解了8种Vue组件通信方式,但在实际开发中该如何选择呢?我来给你一些实用建议:

如果是简单的父子组件通信,优先考虑props和$emit,这是最直接的方式。

当组件层级较深,需要避免props逐层传递时,provide/inject是不错的选择。

对于非父子组件间的简单通信,Event Bus可以快速解决问题,但要注意事件管理。

在需要创建高阶组件或包装组件时,attrsattrs和listeners能大大简化代码。

对于中大型复杂应用,需要集中管理状态时,Vuex或Pinia是必备的。个人更推荐Pinia,因为它更现代、更简洁。

如果需要直接操作子组件,ref提供了最直接的方式,但要谨慎使用以保持组件的封装性。

记住,没有最好的通信方式,只有最适合当前场景的方式。在实际项目中,往往是多种方式结合使用。

写在最后

组件通信是Vue开发中的核心技能,掌握这些通信方式就像掌握了组件间的"对话语言"。从简单的props/$emit到复杂的Pinia状态管理,每种方式都有其独特的价值和适用场景。

关键是要理解每种方式的原理和优缺点,在实际开发中根据组件关系、数据流复杂度、项目规模等因素做出合适的选择。

你现在对Vue组件通信是不是有了更清晰的认识?在实际项目中,你最喜欢用哪种通信方式?有没有遇到过特别的通信难题?欢迎在评论区分享你的经验和心得!

为什么我们从 Python 迁移到 Node.js

作者 Moment
2025年11月11日 07:28

最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777

我们做了一件大事:在上线仅一周后,我们决定将后端彻底从 Python 转移到 Node.js。这一决定的核心原因是 扩展性。

当时,我们的系统还很小,用户量也不大,所以这个决定看起来既冒险又大胆,但从长远来看,我们认为这有助于解决潜在的技术瓶颈。

问题的根源:Python 异步编程的复杂性

我是 Django 的忠实粉丝,它让我能够快速构建应用,并且提供了很多便利的工具。然而,随着我们项目的不断推进,我们发现,Python 在处理异步操作时存在很多问题,尤其是在需要大量并发请求的情况下。

在我们需要异步执行多个 I/O 操作时,Python 的异步编程变得极其混乱。虽然 Python 也有异步支持,但由于其异步模型是后来加入的,导致其稳定性和性能并不理想。相比之下,JavaScript 和 Go 的异步模型更加直观且高效。对于 Python 来说,要使异步编程正常工作,需要大量了解底层实现,并且会受到诸多限制。

Python 异步的挑战:

  • Python 没有原生异步文件 I/O 支持。
  • Django 对异步的支持并不完善,尤其是 ORM(对象关系映射)部分。
  • 为了实现异步功能,开发者必须使用如 async_to_syncsync_to_async 这样的工具,增加了代码复杂性。
  • 异步操作的性能依赖于运行环境,如 Gunicorn 的工作模式等,这使得在 Python 中写出高效的异步代码变得更加困难。

为什么选择 Node.js?

在面对 Python 异步编程的困境时,我们决定转向 Node.js。Node.js 本身就为异步操作提供了很好的支持,它的事件循环机制使得并发处理变得更加高效。

虽然我们曾考虑过使用 Python 中的 FastAPI,它的异步支持确实比 Django 强,但我们最终还是选择了 Node.js,因为它更符合我们对未来可扩展性的需求。Node.js 的事件循环机制和丰富的生态系统使得它非常适合我们的业务需求。

迁移的收益

  1. 性能提升 我们的测试显示,在迁移到 Node.js 后,吞吐量提高了约 3 倍。随着我们在 Node.js 中实现更多并发操作(如文档 chunking 和重新排序),我们预期性能还会继续提升。

  2. 开发效率 Node.js 的事件驱动模型让我们能够更好地处理高并发任务,而无需担心像 Python 那样的异步陷阱。我们能更快速地实现新功能,同时减少了因底层异步问题带来的阻碍。

  3. 统一代码库 迁移到 Node.js 后,我们将原本分开的后台 worker 和 Web 服务器整合到一个代码库中,减少了重复代码,提升了代码的可维护性和一致性。

  4. 更好的测试和重构 迁移过程中,我们写了大量新的测试,确保新系统按预期工作。这也促使我们对现有代码进行了重构,提升了代码质量。

迁移的挑战

  1. 失去 Django 的便利 Django 是一个成熟的框架,它提供了很多现成的功能,例如 ORM 和中间件。迁移到 Node.js 后,我们需要自己手动实现很多功能,虽然可以使用 Express 等框架,但与 Django 的自动化功能相比,仍有一些缺失。

  2. Python 生态的丧失 虽然我们已经转向了 Node.js,但在机器学习和人工智能工具(如 RAG 和 agent)方面,Python 仍然是最强的语言。我们目前可以接受这个改变,但不排除未来可能会重新使用 Python。

总结

这次迁移带来了显著的性能提升和开发效率的提升,同时我们也学到了很多。虽然过程中面临了很多挑战,但整体来看,我们对这次决定感到非常满意。

如果你也正在面临类似的选择,迁移到 Node.js 或许是一个值得考虑的选项,尤其是在你对高并发和扩展性有较高要求的情况下。

光电转换效率超27%,钙钛矿太阳能电池研制成功

2025年11月11日 07:27
从中国科学院半导体研究所获悉,该所游经碧研究员领导的科研团队近期研制出光电转换效率为27.2%的钙钛矿太阳能电池原型器件。器件在1个标准太阳光和最大功率输出点条件下持续运行1529小时后,仍可保持初始效率的86.3%。此外,器件在1个标准太阳光与85℃光热耦合加速老化条件下,持续运行1000小时后仍能维持初始效率的82.8%。相关成果近日在国际学术期刊《科学》发表。(央视新闻)

新加坡Grab将向远程驾驶公司Vay投资6000万美元

2025年11月11日 07:25
新加坡企业Grab控股公司于11月10日宣布,将向远程驾驶公司Vay Technology投资6000万美元。Grab正试图借助其网约车平台进军自动驾驶领域。Grab方面称,若Vay达成特定里程碑目标,Grab将在一年内追加投资3.5亿美元。(新浪财经)

抖音电商:“卖茅台低于市场价将被罚”相关传言不实

2025年11月11日 07:24
11月10日,有酒商表示,已接到抖音平台通知,酒商销售茅台价格如果低于市场行情价将会被处罚。对此,抖音电商相关负责人表示,关于抖音新规 “卖茅台低于市场价将被罚,达人自己补贴也不行” 的相关传言不实,属于误读。近日,针对消费者投诉个别商家和达人利用茅台进行虚假宣传等违规营销问题,抖音电商发起了虚假宣传治理专项行动,重点治理以 “虚假低价” 为噱头的违规引流行为,以及有假货嫌疑的异常低价行为。(界面)

苏伊士运河与远洋航运公司商讨全球航运回归事宜

2025年11月11日 07:21
埃及官员与主要航运公司召开会议,商讨让全球航运回归陷入困境的苏伊士运河航线事宜。埃及苏伊士运河管理局主席奥萨马・拉比上周会晤了20家航运公司及机构的代表,就红海地区局势发展、其对经运河的全球贸易及海运市场的影响展开讨论。此次会议在苏伊士运河管理局总部举行,管理局董事会多名成员也出席了会议。这是该管理局为协商航行计划与船期而定期召开的系列会议中,最新的一次会议。(新浪财经)

时隔近30年,黄金在全球央行储备中占比超美债

2025年11月11日 07:19
今年以来,国际金价累计上涨超50%,这也带动了黄金投资的热度。中国黄金协会发布的数据显示,今年前三季度国内黄金ETF增仓量同比增长164.03%。此外,路透社近期的一篇报道,援引彭博社和第三方对冲基金数据得出了一个重要结论,即黄金在全球央行储备中的占比,时隔近30年再次超越美债。截至今年8月下旬,全球央行总计持有36000吨黄金,按照黄金当时每盎司3500美元的均价计算,这些黄金储备的总价值约为4万亿美元,显著高于当时除美联储外全球央行所持有的3.5万亿美元美债。从占比来看,黄金在全球央行储备中的占比目前为27%,美债占比为23%。(央视财经)

港人“北上”贷款渐起,深港金融数据“南北皆通”

2025年11月11日 07:16
大湾区居民双向奔赴。继消费之后,过去的以“南向”居多的深港跨境信贷,开始落地“北向”案例。在香港金融科技周期间,一位香港征信机构负责人表示,目前香港居民“北上”贷款或申请信用卡的需求有所提升。过去,深港跨境信贷的案例更多集中于“南向”,即内地企业经由香港出海和在港融资、内地居民在港开户等,其实现方式大多依托于深圳跨境数据验证平台(DVP)。其中,微众银行、富融银行、东亚银行、工银亚洲等银行均已有案例落地。(21财经)

微软借力阿历克斯・厄尔等网红,试图缩小与ChatGPT的差距

2025年11月11日 07:14
微软急于提升其Copilot聊天机器人的下载量,已招募美国部分最具人气的网红,向年轻消费者传递一则核心信息:我们的AI助手和ChatGPT一样酷。微软确实需要这样的助力。该公司近期表示,其Copilot系列助手每月活跃用户达1.5亿。但OpenAI的ChatGPT宣称每周活跃用户有8亿,谷歌的Gemini则号称月活用户6.5亿。(新浪财经)

华泰证券:机构及外资情绪有所回暖

2025年11月11日 07:12
36氪获悉,华泰证券研报指出,近期科技股调整等扰动下,市场走势相对震荡,交易型资金活跃度有所降温,参与交易的投资者数量中枢边际回落至9月初水平,散户资金转向净流出,融资活跃度创2025年9月以来最低,但整体来看,资金面亮点仍存,存在活跃基底:1.私募资金配置意愿偏强,上周备案数量环比回升至286家,10月底股票私募仓位创出今年以来的新高;2.10月中旬以来,公募基金仓位出现趋势性回升迹象;3.以EPFR统计的配置型外资上期(10月29日-11月5日)净流入60.8亿元,创今年以来净流入新高。

特斯拉Cybertruck业务负责人将离职

2025年11月11日 07:08
负责特斯拉Cybertruck业务的高管,将在任职8年后离开这家由埃隆・马斯克执掌的汽车制造商。特斯拉Cybertruck与Model 3项目负责人希德汉特・阿瓦斯蒂在领英上表示,离开公司并非易事。他未透露下一步的职业规划。(新浪财经)

美法官暂时阻止美政府要求各州收回食品援助金的举措

2025年11月11日 07:05
当地时间11月10日获悉,根据当日发布的一份法院公告,美国一位联邦法官暂时阻止了特朗普政府要求各州“撤销”发放全额食品援助福利的指令。此前11月9日,特朗普政府指示各州不要发放11月份的全额补充营养援助计划(SNAP)福利。在7日,美国联邦最高法院大法官凯坦吉·布朗·杰克逊发布紧急命令,暂时中止下级法院要求特朗普政府在11月全面发放SNAP福利的裁定。美政府随后根据最高法的命令,修改了之前的指导意见。(央视新闻)

美股三大指数集体收涨,英伟达涨超5%

2025年11月11日 07:00
36氪获悉,11月10日收盘,美股三大指数集体上涨,道指涨0.81%,标普500指数涨1.54%,纳指涨2.27%。大型科技股普涨,英伟达涨超5%,创4月份以来最大单日涨幅;AMD、谷歌涨超4%,特斯拉涨超3%,微软、亚马逊、Meta、奈飞涨超1%。热门中概股涨跌不一,小鹏汽车涨超16%,百度涨超5%,微博涨超3%,爱奇艺涨超2%,拼多多涨超1%;蔚来跌超2%,京东跌超1%,阿里巴巴、哔哩哔哩小幅下跌。

每日一题-一和零🟡

2025年11月11日 00:00

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

 

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

 

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0' 和 '1' 组成
  • 1 <= m, n <= 100

一步步思考:从记忆化搜索到递推到空间优化!(Python/Java/C++/Go)

作者 endlesscheng
2025年1月4日 13:47

前言

设 $\textit{strs}[i]$ 中 $0$ 的个数为 $\textit{cnt}_0[i]$,$1$ 的个数为 $\textit{cnt}_1[i]$,那么本题相当于:

  • 有一个容量为 $(m,n)$ 的背包,至多可以装入 $m$ 个 $0$ 和 $n$ 个 $1$。现在有 $n$ 个物品,每个物品的体积为 $(\textit{cnt}_0[i],\textit{cnt}_1[i])$,表示该物品有 $\textit{cnt}_0[i]$ 个 $0$ 和 $\textit{cnt}_1[i]$ 个 $1$。问:最多可以选多少个物品?

这相当于背包有两种体积(二维),所以在定义状态的时候,相比只有一种体积的 0-1 背包,要多加一个参数。

如果你不了解 0-1 背包,请看【基础算法精讲 18】

一、记忆化搜索

在一维 0-1 背包的基础上,多加一个参数,即定义 $\textit{dfs}(i,j,k)$ 表示在 $[0,i]$ 中选字符串,在 $0$ 的个数至多为 $j$,$1$ 的个数至多为 $k$ 的约束下,至多可以选多少个字符串。

考虑 $\textit{strs}[i]$ 选或不选:

  • 不选:问题变成在 $[0,i-1]$ 中选字符串,在 $0$ 的个数至多为 $j$,$1$ 的个数至多为 $k$ 的约束下,至多可以选多少个字符串,即 $\textit{dfs}(i,j,k) = \textit{dfs}(i-1,j,k)$。
  • 选:如果 $j\ge \textit{cnt}_0[i]$ 并且 $k\ge \textit{cnt}_1[i]$ 则可以选。问题变成在 $[0,i-1]$ 中选字符串,在 $0$ 的个数至多为 $j-\textit{cnt}_0[i]$,$1$ 的个数至多为 $k-\textit{cnt}_1[i]$ 的约束下,至多可以选多少个字符串,即 $\textit{dfs}(i,j,k) = \textit{dfs}(i-1,j-\textit{cnt}_0[i],k-\textit{cnt}_1[i]) + 1$。

两种情况取最大值,得

$$
\textit{dfs}(i,j,k) = \max(\textit{dfs}(i-1,j,k), \textit{dfs}(i-1,j-\textit{cnt}_0[i],k-\textit{cnt}_1[i]) + 1)
$$

如果

递归边界:$\textit{dfs}(-1,j,k)=0$。此时没有物品可以选。

递归入口:$\textit{dfs}(k-1,m,n)$,这是原问题,也是答案。其中 $k$ 为 $\textit{strs}$ 的长度。

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        cnt0 = [s.count('0') for s in strs]

        @cache  # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
        def dfs(i: int, j: int, k: int) -> int:
            if i < 0:
                return 0
            res = dfs(i - 1, j, k)  # 不选 strs[i]
            cnt1 = len(strs[i]) - cnt0[i]
            if j >= cnt0[i] and k >= cnt1:
                res = max(res, dfs(i - 1, j - cnt0[i], k - cnt1) + 1)  # 选 strs[i]
            return res

        return dfs(len(strs) - 1, m, n)
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int k = strs.length;
        int[] cnt0 = new int[k];
        for (int i = 0; i < k; i++) {
            cnt0[i] = (int) strs[i].chars().filter(ch -> ch == '0').count();
        }

        int[][][] memo = new int[strs.length][m + 1][n + 1];
        for (int[][] mat : memo) {
            for (int[] arr : mat) {
                Arrays.fill(arr, -1); // -1 表示没有计算过
            }
        }
        return dfs(k - 1, m, n, strs, cnt0, memo);
    }

    private int dfs(int i, int j, int k, String[] strs, int[] cnt0, int[][][] memo) {
        if (i < 0) {
            return 0;
        }
        if (memo[i][j][k] != -1) { // 之前计算过
            return memo[i][j][k];
        }
        // 不选 strs[i]
        int res = dfs(i - 1, j, k, strs, cnt0, memo);  
        int cnt1 = strs[i].length() - cnt0[i];
        if (j >= cnt0[i] && k >= cnt1) {
            // 选 strs[i]
            res = Math.max(res, dfs(i - 1, j - cnt0[i], k - cnt1, strs, cnt0, memo) + 1);
        }
        return memo[i][j][k] = res; // 记忆化
    }
}
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<int> cnt0(strs.size());
        for (int i = 0; i < strs.size(); i++) {
            cnt0[i] = ranges::count(strs[i], '0');
        }

        vector memo(strs.size(), vector(m + 1, vector<int>(n + 1, -1))); // -1 表示没有计算过
        auto dfs = [&](this auto&& dfs, int i, int j, int k) -> int {
            if (i < 0) {
                return 0;
            }
            int& res = memo[i][j][k]; // 注意这里是引用
            if (res != -1) { // 之前计算过
                return res;
            }
            res = dfs(i - 1, j, k); // 不选 strs[i]
            int cnt1 = strs[i].size() - cnt0[i];
            if (j >= cnt0[i] && k >= cnt1) {
                res = max(res, dfs(i - 1, j - cnt0[i], k - cnt1) + 1); // 选 strs[i]
            }
            return res;
        };
        return dfs(strs.size() - 1, m, n);
    }
};
func findMaxForm(strs []string, m, n int) int {
    k := len(strs)
    cnt0 := make([]int, k)
    for i, s := range strs {
        cnt0[i] = strings.Count(s, "0")
    }

    memo := make([][][]int, k)
    for i := range memo {
        memo[i] = make([][]int, m+1)
        for j := range memo[i] {
            memo[i][j] = make([]int, n+1)
            for k := range memo[i][j] {
                memo[i][j][k] = -1 // -1 表示没有计算过
            }
        }
    }
    var dfs func(int, int, int) int
    dfs = func(i, j, k int) int {
        if i < 0 {
            return 0
        }
        p := &memo[i][j][k]
        if *p != -1 { // 之前计算过
            return *p
        }
        res := dfs(i-1, j, k) // 不选 strs[i]
        cnt1 := len(strs[i]) - cnt0[i]
        if j >= cnt0[i] && k >= cnt1 {
            res = max(res, dfs(i-1, j-cnt0[i], k-cnt1)+1) // 选 strs[i]
        }
        *p = res // 记忆化
        return res
    }
    return dfs(k-1, m, n)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(kmn+L)$,其中 $k$ 为 $\textit{strs}$ 的长度,$L$ 为 $\textit{strs}$ 中所有字符串的长度之和。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(kmn)$,单个状态的计算时间为 $\mathcal{O}(1)$,所以总的时间复杂度为 $\mathcal{O}(kmn)$。
  • 空间复杂度:$\mathcal{O}(kmn)$。保存多少状态,就需要多少空间。

二、1:1 翻译成递推

我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体来说,$f[i+1][j][k]$ 的定义和 $\textit{dfs}(i,j,k)$ 的定义是一样的,都表示在 $[0,i]$ 中选字符串,在 $0$ 的个数至多为 $j$,$1$ 的个数至多为 $k$ 的约束下,至多可以选多少个字符串。这里 $+1$ 是为了把 $\textit{dfs}(-1,j,k)$ 这个状态也翻译过来,这样我们可以把 $f[0][j][k]$ 作为初始值。

相应的递推式(状态转移方程)也和 $\textit{dfs}$ 一样:

$$
f[i+1][j][k] = \max(f[i][j][k], f[i][j-\textit{cnt}_0[i]][k-\textit{cnt}_1[i]] + 1)
$$

初始值 $f[0][j][k]=0$,翻译自递归边界 $\textit{dfs}(-1,j,k)=0$。

答案为 $f[k][m][n]$,翻译自递归入口 $\textit{dfs}(k-1,m,n)$。其中 $k$ 为 $\textit{strs}$ 的长度。

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        f = [[[0] * (n + 1) for _ in range(m + 1)] for _ in range(len(strs) + 1)]
        for i, s in enumerate(strs):
            cnt0 = s.count('0')
            cnt1 = len(s) - cnt0
            for j in range(m + 1):
                for k in range(n + 1):
                    if j >= cnt0 and k >= cnt1:
                        f[i + 1][j][k] = max(f[i][j][k], f[i][j - cnt0][k - cnt1] + 1)
                    else:
                        f[i + 1][j][k] = f[i][j][k]
        return f[-1][m][n]
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][][] f = new int[strs.length + 1][m + 1][n + 1];
        for (int i = 0; i < strs.length; i++) {
            int cnt0 = (int) strs[i].chars().filter(ch -> ch == '0').count();
            int cnt1 = strs[i].length() - cnt0;
            for (int j = 0; j <= m; j++) {
                for (int k = 0; k <= n; k++) {
                    if (j >= cnt0 && k >= cnt1) {
                        f[i + 1][j][k] = Math.max(f[i][j][k], f[i][j - cnt0][k - cnt1] + 1);
                    } else {
                        f[i + 1][j][k] = f[i][j][k];
                    }
                }
            }
        }
        return f[strs.length][m][n];
    }
}
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector f(strs.size() + 1, vector(m + 1, vector<int>(n + 1)));
        for (int i = 0; i < strs.size(); i++) {
            int cnt0 = ranges::count(strs[i], '0');
            int cnt1 = strs[i].size() - cnt0;
            for (int j = 0; j <= m; j++) {
                for (int k = 0; k <= n; k++) {
                    if (j >= cnt0 && k >= cnt1) {
                        f[i + 1][j][k] = max(f[i][j][k], f[i][j - cnt0][k - cnt1] + 1);
                    } else {
                        f[i + 1][j][k] = f[i][j][k];
                    }
                }
            }
        }
        return f.back()[m][n];
    }
};
func findMaxForm(strs []string, m, n int) int {
    k := len(strs)
    f := make([][][]int, k+1)
    for i := range f {
        f[i] = make([][]int, m+1)
        for j := range f[i] {
            f[i][j] = make([]int, n+1)
        }
    }
    for i, s := range strs {
        cnt0 := strings.Count(s, "0")
        cnt1 := len(s) - cnt0
        for j := range m + 1 {
            for k := range n + 1 {
                if j >= cnt0 && k >= cnt1 {
                    f[i+1][j][k] = max(f[i][j][k], f[i][j-cnt0][k-cnt1]+1)
                } else {
                    f[i+1][j][k] = f[i][j][k]
                }
            }
        }
    }
    return f[k][m][n]
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(kmn+L)$,其中 $k$ 为 $\textit{strs}$ 的长度,$L$ 为 $\textit{strs}$ 中所有字符串的长度之和。
  • 空间复杂度:$\mathcal{O}(kmn)$。

三、空间优化

观察上面的状态转移方程,在计算 $f[i+1]$ 时,只会用到 $f[i]$,不会用到比 $i$ 更早的状态。

那么去掉第一个维度,把 $f[i+1]$ 和 $f[i]$ 保存到同一个二维数组中。

状态转移方程改为

$$
f[j][k] = \max(f[j][k], f[j-\textit{cnt}_0[i]][k-\textit{cnt}_1[i]] + 1)
$$

初始值 $f[j][k]=0$。

答案为 $f[m][n]$。

下面代码为什么要倒序循环,请看【基础算法精讲 18】

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        f = [[0] * (n + 1) for _ in range(m + 1)]
        for s in strs:
            cnt0 = s.count('0')
            cnt1 = len(s) - cnt0
            for j in range(m, cnt0 - 1, -1):
                for k in range(n, cnt1 - 1, -1):
                    f[j][k] = max(f[j][k], f[j - cnt0][k - cnt1] + 1)
        return f[m][n]
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] f = new int[m + 1][n + 1];
        for (String s : strs) {
            int cnt0 = (int) s.chars().filter(ch -> ch == '0').count();
            int cnt1 = s.length() - cnt0;
            for (int j = m; j >= cnt0; j--) {
                for (int k = n; k >= cnt1; k--) {
                    f[j][k] = Math.max(f[j][k], f[j - cnt0][k - cnt1] + 1);
                }
            }
        }
        return f[m][n];
    }
}
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector f(m + 1, vector<int>(n + 1));
        for (string& s : strs) {
            int cnt0 = ranges::count(s, '0');
            int cnt1 = s.size() - cnt0;
            for (int j = m; j >= cnt0; j--) {
                for (int k = n; k >= cnt1; k--) {
                    f[j][k] = max(f[j][k], f[j - cnt0][k - cnt1] + 1);
                }
            }
        }
        return f[m][n];
    }
};
func findMaxForm(strs []string, m, n int) int {
    f := make([][]int, m+1)
    for i := range f {
        f[i] = make([]int, n+1)
    }
    for _, s := range strs {
        cnt0 := strings.Count(s, "0")
        cnt1 := len(s) - cnt0
        for j := m; j >= cnt0; j-- {
            for k := n; k >= cnt1; k-- {
                f[j][k] = max(f[j][k], f[j-cnt0][k-cnt1]+1)
            }
        }
    }
    return f[m][n]
}

进一步优化

比如 $n=m=90$,前 $3$ 个字符串总共有 $5$ 个 $0$ 和 $6$ 个 $1$,那么无论我们怎么选,也选不出几十个 $0$ 和 $1$,所以上面的代码中,其实有大量的循环是多余的。

为此,额外用两个变量 $\textit{sum}_0$ 和 $\textit{sum}_1$ 分别维护前 $i$ 个字符串中的 $0$ 的个数和 $1$ 的个数(但不能超过 $m$ 和 $n$)。循环的时候 $j$ 从 $\textit{sum}_0$ 开始,$k$ 从 $\textit{sum}_1$ 开始。

注意这个优化会导致只有一部分 $f[j][k]$ 被更新到,最大值并没有传递给 $f[m][n]$,可能留在二维数组中间的某个位置上。所以最后要遍历 $f$,取其中最大值作为答案。

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        f = [[0] * (n + 1) for _ in range(m + 1)]
        sum0 = sum1 = 0
        for s in strs:
            cnt0 = s.count('0')
            cnt1 = len(s) - cnt0
            sum0 = min(sum0 + cnt0, m)
            sum1 = min(sum1 + cnt1, n)
            for j in range(sum0, cnt0 - 1, -1):
                for k in range(sum1, cnt1 - 1, -1):
                    v = f[j - cnt0][k - cnt1] + 1
                    if v > f[j][k]:  # 手写 max,效率更高
                        f[j][k] = v
        return max(map(max, f))
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] f = new int[m + 1][n + 1];
        int sum0 = 0;
        int sum1 = 0;
        for (String s : strs) {
            int cnt0 = (int) s.chars().filter(ch -> ch == '0').count();
            int cnt1 = s.length() - cnt0;
            sum0 = Math.min(sum0 + cnt0, m);
            sum1 = Math.min(sum1 + cnt1, n);
            for (int j = sum0; j >= cnt0; j--) {
                for (int k = sum1; k >= cnt1; k--) {
                    f[j][k] = Math.max(f[j][k], f[j - cnt0][k - cnt1] + 1);
                }
            }
        }
        int ans = 0;
        for (int[] row : f) {
            for (int v : row) {
                ans = Math.max(ans, v);
            }
        }
        return ans;
    }
}
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector f(m + 1, vector<int>(n + 1));
        int sum0 = 0, sum1 = 0;
        for (string& s : strs) {
            int cnt0 = ranges::count(s, '0');
            int cnt1 = s.size() - cnt0;
            sum0 = min(sum0 + cnt0, m);
            sum1 = min(sum1 + cnt1, n);
            for (int j = sum0; j >= cnt0; j--) {
                for (int k = sum1; k >= cnt1; k--) {
                    f[j][k] = max(f[j][k], f[j - cnt0][k - cnt1] + 1);
                }
            }
        }
        int ans = 0;
        for (auto& row : f) {
            ans = max(ans, ranges::max(row));
        }
        return ans;
    }
};
func findMaxForm(strs []string, m, n int) (ans int) {
    f := make([][]int, m+1)
    for i := range f {
        f[i] = make([]int, n+1)
    }
    sum0, sum1 := 0, 0
    for _, s := range strs {
        cnt0 := strings.Count(s, "0")
        cnt1 := len(s) - cnt0
        sum0 = min(sum0+cnt0, m)
        sum1 = min(sum1+cnt1, n)
        for j := sum0; j >= cnt0; j-- {
            for k := sum1; k >= cnt1; k-- {
                f[j][k] = max(f[j][k], f[j-cnt0][k-cnt1]+1)
            }
        }
    }
    for _, row := range f {
        ans = max(ans, slices.Max(row))
    }
    return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(kmn+L)$,其中 $k$ 为 $\textit{strs}$ 的长度,$L$ 为 $\textit{strs}$ 中所有字符串的长度之和。
  • 空间复杂度:$\mathcal{O}(mn)$。

更多相似题目,见 动态规划题单 中的「§3.1 0-1 背包」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 【本题相关】动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌
❌